feat(parcel-sync): extended enrichment fields from existing API data

New fields extracted from already-fetched documentation/GIS data
(zero extra API calls, no performance impact):

- TIP_INSCRIERE: "Intabulare, drept de PROPRIETATE, dobandit prin..."
- ACT_PROPRIETATE: "hotarare judecatoreasca nr... / contract vanzare..."
- COTA_PROPRIETATE: "1/1" or fractional
- DATA_CERERE: date of registration application
- NR_CORPURI: number of building bodies on parcel
- CORPURI_DETALII: "C1:352mp, C2:248mp, C3:104mp"
- IS_CONDOMINIUM: condominium flag
- DATA_CREARE: parcel creation date in eTerra

Also fixed HAS_BUILDING: now also uses NR_CORPURI count as fallback
(was 0 for parcels where buildingMap cross-ref missed matches).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
AI Assistant
2026-03-27 08:57:13 +02:00
parent 9d45799900
commit e42eeb6324
@@ -135,6 +135,23 @@ export type FeatureEnrichment = {
CATEGORIE_FOLOSINTA: string; CATEGORIE_FOLOSINTA: string;
HAS_BUILDING: number; HAS_BUILDING: number;
BUILD_LEGAL: number; BUILD_LEGAL: number;
// Extended fields (extracted from existing API calls, zero overhead)
/** "Intabulare, drept de PROPRIETATE, dobandit prin..." */
TIP_INSCRIERE?: string;
/** "hotarare judecatoreasca nr..." / "contract vanzare cumparare nr..." */
ACT_PROPRIETATE?: string;
/** "1/1" or fractional */
COTA_PROPRIETATE?: string;
/** Date of registration application (ISO) */
DATA_CERERE?: string;
/** Number of building bodies on this parcel */
NR_CORPURI?: number;
/** Comma-separated list: "C1:352mp, C2:248mp, C3:104mp" */
CORPURI_DETALII?: string;
/** 1 if condominium, 0 otherwise */
IS_CONDOMINIUM?: number;
/** Date parcel was created in eTerra (ISO) */
DATA_CREARE?: string;
}; };
/** /**
@@ -369,6 +386,8 @@ export async function enrichFeatures(
// ── Fetch documentation/owner data ── // ── Fetch documentation/owner data ──
push({ phase: "Descărcare documentații CF" }); push({ phase: "Descărcare documentații CF" });
const docByImmovable = new Map<string, any>(); const docByImmovable = new Map<string, any>();
// Store raw registrations per landbookIE for extended enrichment fields
const regsByLandbook = new Map<string, any[]>();
const immovableIds = Array.from(immovableListById.keys()); const immovableIds = Array.from(immovableListById.keys());
const docBatchSize = 50; const docBatchSize = 50;
for (let i = 0; i < immovableIds.length; i += docBatchSize) { for (let i = 0; i < immovableIds.length; i += docBatchSize) {
@@ -385,6 +404,13 @@ export async function enrichFeatures(
const nodeMap = new Map<number, any>(); const nodeMap = new Map<number, any>();
for (const reg of regs) { for (const reg of regs) {
if (reg?.nodeId != null) nodeMap.set(Number(reg.nodeId), reg); if (reg?.nodeId != null) nodeMap.set(Number(reg.nodeId), reg);
// Store all registrations by landbookIE for extended enrichment
if (reg?.landbookIE) {
const lbKey = String(reg.landbookIE);
const existing = regsByLandbook.get(lbKey) ?? [];
existing.push(reg);
regsByLandbook.set(lbKey, existing);
}
} }
// Check if an entry or any ancestor "I" inscription is radiated // Check if an entry or any ancestor "I" inscription is radiated
const isRadiated = (entry: any, depth = 0): boolean => { const isRadiated = (entry: any, depth = 0): boolean => {
@@ -641,6 +667,59 @@ export async function enrichFeatures(
: null); : null);
} }
// Extended fields — extracted from existing data, zero extra API calls
let tipInscriere = "";
let actProprietate = "";
let cotaProprietate = "";
let dataCerere = "";
// Extract registration details from already-fetched documentation
const lbKey = landbookIE || cadRefRaw;
const regsForParcel = regsByLandbook.get(String(lbKey)) ?? [];
for (const reg of regsForParcel) {
const nt = String(reg?.nodeType ?? "").toUpperCase();
const nn = String(reg?.nodeName ?? "").trim();
if (nt === "I" && nn && !tipInscriere) {
tipInscriere = nn;
const quota = reg?.registration?.actualQuota;
if (quota) cotaProprietate = String(quota);
}
if (nt === "A" && nn && !actProprietate) {
actProprietate = nn;
}
if (nt === "C" && !dataCerere) {
const appDate = reg?.application?.appDate;
if (typeof appDate === "number" && appDate > 0) {
dataCerere = new Date(appDate).toISOString().slice(0, 10);
}
}
}
// Building body details from local DB cladiri
const cadRefBase = baseCadRef(cadRefRaw);
let nrCorpuri = 0;
const corpuriParts: string[] = [];
for (const cFeature of cladiri) {
const cAttrs = cFeature.attributes as Record<string, unknown>;
const cRef = String(cAttrs.NATIONAL_CADASTRAL_REFERENCE ?? "");
if (baseCadRef(cRef) === cadRefBase && cRef.includes("-")) {
nrCorpuri++;
const suffix = cRef.slice(cRef.lastIndexOf("-") + 1);
const cArea = typeof cAttrs.AREA_VALUE === "number" ? cAttrs.AREA_VALUE : 0;
corpuriParts.push(`${suffix}:${Math.round(cArea)}mp`);
}
}
// Condominium status and creation date from documentation
const docImmovable = docKey ? docByImmovable.get(docKey) : undefined;
const isCondominium = Number(
(docImmovable as Record<string, unknown>)?.isCondominium ?? 0,
);
const createdDtm = attrs.CREATED_DTM;
const dataCreare =
typeof createdDtm === "number" && createdDtm > 0
? new Date(createdDtm).toISOString().slice(0, 10)
: "";
const enrichment: FeatureEnrichment = { const enrichment: FeatureEnrichment = {
NR_CAD: cadRefRaw, NR_CAD: cadRefRaw,
NR_CF: nrCF, NR_CF: nrCF,
@@ -654,8 +733,16 @@ export async function enrichFeatures(
SOLICITANT: solicitant, SOLICITANT: solicitant,
INTRAVILAN: intravilan, INTRAVILAN: intravilan,
CATEGORIE_FOLOSINTA: categorie, CATEGORIE_FOLOSINTA: categorie,
HAS_BUILDING: hasBuilding, HAS_BUILDING: hasBuilding || (nrCorpuri > 0 ? 1 : 0),
BUILD_LEGAL: buildLegal, BUILD_LEGAL: buildLegal,
TIP_INSCRIERE: tipInscriere || undefined,
ACT_PROPRIETATE: actProprietate || undefined,
COTA_PROPRIETATE: cotaProprietate || undefined,
DATA_CERERE: dataCerere || undefined,
NR_CORPURI: nrCorpuri,
CORPURI_DETALII: corpuriParts.length > 0 ? corpuriParts.join(", ") : undefined,
IS_CONDOMINIUM: isCondominium,
DATA_CREARE: dataCreare || undefined,
}; };
// Store enrichment in DB // Store enrichment in DB