← back to Nationalrealestate
scripts/enrich-whatcom-situs.mjs
128 lines
#!/usr/bin/env node
/**
* Enrich Whatcom County WA (53073) parcels with situs address + owner + land use
* from the WhatcomCo_Property parcel layer (2-layer join: Sales geo_id → Property geo_id).
*
* Fills NULL-only fields — never regresses existing values.
* Run: node scripts/enrich-whatcom-situs.mjs [--dry-run]
*
* TK-10777 — Whatcom WA situs/owner join
*/
import pg from 'pg';
const { Pool } = pg;
const DRY_RUN = process.argv.includes('--dry-run');
const PROPERTY_LAYER = 'https://gis.whatcomcounty.us/arcgis/rest/services/EnterprisePublishing/WhatcomCo_Property/MapServer/0';
const BATCH = 200; // well under maxRecordCount=1000
const pool = new Pool({
connectionString: process.env.DATABASE_URL || 'postgresql:///usre?host=/tmp',
max: 4,
});
function s(v) { const t = v == null ? '' : String(v).trim(); return t || null; }
function buildAddress(a) {
return [s(a.situs_num), s(a.situs_street_prefix), s(a.situs_street), s(a.situs_unit)]
.filter(Boolean).join(' ').trim() || null;
}
function normAddr(addr) {
if (!addr) return null;
return addr.toUpperCase().replace(/\s+/g, ' ').replace(/[.,]/g, '').trim();
}
async function fetchPropertyBatch(gids) {
const escaped = gids.map(g => `'${g.replace(/'/g, "''")}'`).join(',');
const where = `geo_id IN (${escaped})`;
// Use POST to avoid URL-length limits with large batches (200 16-char IDs ≈ 4KB).
const body = new URLSearchParams({
where,
outFields: 'geo_id,title_owner_name,property_use_description,situs_num,situs_street_prefix,situs_street,situs_unit,situs_city',
resultRecordCount: String(BATCH + 10),
f: 'json',
});
const res = await fetch(`${PROPERTY_LAYER}/query`, {
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
body: body.toString(),
signal: AbortSignal.timeout(60_000),
});
if (!res.ok) throw new Error(`Property layer HTTP ${res.status}`);
const j = await res.json();
if (j.error) throw new Error(`Property layer ArcGIS error: ${JSON.stringify(j.error)}`);
return j.features || [];
}
async function main() {
console.log(`[enrich-whatcom-situs] ${DRY_RUN ? 'DRY-RUN mode' : 'LIVE mode'}`);
// Get all Whatcom parcels that are missing address OR owner OR use_desc
const { rows: parcels } = await pool.query(
`SELECT source_id FROM parcel WHERE county_fips = '53073' ORDER BY source_id`
);
console.log(`[enrich-whatcom-situs] Found ${parcels.length} total Whatcom parcels`);
const gids = parcels.map(r => r.source_id);
let enriched = 0;
let skipped = 0;
for (let i = 0; i < gids.length; i += BATCH) {
const batch = gids.slice(i, i + BATCH);
let features;
try {
features = await fetchPropertyBatch(batch);
} catch (e) {
console.error(`[enrich-whatcom-situs] Batch ${i}-${i + batch.length} fetch error:`, e.message);
// Continue with next batch rather than aborting the whole run
skipped += batch.length;
continue;
}
const byGid = new Map();
for (const f of features) {
const a = f.attributes;
const gid = s(a.geo_id);
if (!gid) continue;
byGid.set(gid, {
address: buildAddress(a),
city: s(a.situs_city),
owner_name: s(a.title_owner_name),
use_desc: s(a.property_use_description),
});
}
let batchEnriched = 0;
for (const gid of batch) {
const prop = byGid.get(gid);
if (!prop) { skipped++; continue; }
if (!prop.address && !prop.city && !prop.owner_name && !prop.use_desc) { skipped++; continue; }
if (!DRY_RUN) {
await pool.query(
`UPDATE parcel SET
address = COALESCE(address, $1),
norm_address = COALESCE(norm_address, $2),
city = COALESCE(city, $3),
owner_name = COALESCE(owner_name, $4),
use_desc = COALESCE(use_desc, $5)
WHERE county_fips = '53073' AND source_id = $6
AND (address IS NULL OR city IS NULL OR owner_name IS NULL OR use_desc IS NULL)`,
[prop.address, normAddr(prop.address), prop.city, prop.owner_name, prop.use_desc, gid]
);
}
batchEnriched++;
}
enriched += batchEnriched;
console.log(`[enrich-whatcom-situs] Batch ${i + batch.length}/${gids.length} — enriched so far: ${enriched}`);
// Small pause to be polite to the county ArcGIS server
if (i + BATCH < gids.length) await new Promise(r => setTimeout(r, 300));
}
console.log(`\n[enrich-whatcom-situs] DONE — ${enriched} parcels enriched, ${skipped} skipped (no property record found or all fields already populated)`);
await pool.end();
}
main().catch(e => { console.error(e); process.exit(1); });