← back to Commercialrealestate
scripts/ingest-assessor.js
85 lines
#!/usr/bin/env node
// ingest-assessor.js — apply assessor-schema.sql then COPY-stream the NDJSON parcel roll into cre.assessor_parcel.
// Streams the file line-by-line (never loads the whole roll in RAM), batch-upserts via multi-row INSERT
// ON CONFLICT. Idempotent: re-running re-upserts by AIN. $0 (local Postgres).
//
// Run: node scripts/ingest-assessor.js # ingests data/raw/assessor-parcels-2025.ndjson
// node scripts/ingest-assessor.js path/to/file.ndjson # explicit file
'use strict';
const fs = require('fs');
const path = require('path');
const readline = require('readline');
const { execFileSync } = require('child_process');
const { pool } = require('./db/brokers-db');
const FILE = process.argv[2] || path.join(__dirname, '..', 'data', 'raw', 'assessor-parcels-2025.ndjson');
const BATCH = 1000;
function toDate(ms) { return (ms == null || ms === '') ? null : new Date(+ms).toISOString().slice(0, 10); }
function toInt(v) { const n = parseInt(v, 10); return Number.isFinite(n) ? n : null; }
function s(v) { v = (v == null ? '' : String(v)).trim(); return v === '' ? null : v; }
const COLS = ['ain', 'roll_year', 'property_location', 'situs_house_no', 'situs_street', 'situs_unit',
'situs_city', 'situs_zip5', 'use_type', 'use_code', 'use_desc1', 'use_desc2', 'year_built',
'effective_year_built', 'sqft_main', 'bedrooms', 'bathrooms', 'units', 'recording_date',
'roll_land_value', 'roll_imp_value', 'roll_tot_land_imp', 'roll_total_value', 'net_taxable_value',
'is_taxable', 'parcel_classification', 'admin_region', 'lat', 'lng'];
function row(a) {
return [
s(a.AIN), s(a.RollYear), s(a.PropertyLocation), toInt(a.SitusHouseNo), s(a.SitusStreet), s(a.SitusUnit),
s(a.SitusCity), s(a.SitusZIP5), s(a.UseType), s(a.UseCode), s(a.UseCodeDescChar1), s(a.UseCodeDescChar2),
toInt(a.YearBuilt), toInt(a.EffectiveYearBuilt), toInt(a.SQFTmain), toInt(a.Bedrooms), toInt(a.Bathrooms),
toInt(a.Units), toDate(a.RecordingDate), toInt(a.Roll_LandValue), toInt(a.Roll_ImpValue),
toInt(a.Roll_totLandImp), toInt(a.Roll_TotalValue), toInt(a.netTaxableValue), s(a.isTaxableParcel),
s(a.ParcelClassification), s(a.AdminRegion),
(typeof a.CENTER_LAT === 'number' ? a.CENTER_LAT : null), (typeof a.CENTER_LON === 'number' ? a.CENTER_LON : null)
];
}
async function flush(batch) {
if (!batch.length) return;
const vals = [], params = [];
let p = 0;
for (const r of batch) {
vals.push('(' + COLS.map(() => '$' + (++p)).join(',') + ')');
params.push(...r);
}
const upd = COLS.filter(c => c !== 'ain').map(c => `${c}=EXCLUDED.${c}`).join(',');
await pool.query(
`INSERT INTO assessor_parcel(${COLS.join(',')}) VALUES ${vals.join(',')}
ON CONFLICT(ain) DO UPDATE SET ${upd}, loaded_at=now()`, params);
}
async function main() {
if (!fs.existsSync(FILE)) { console.error('[ingest] file not found:', FILE); process.exit(1); }
const ddl = fs.readFileSync(path.join(__dirname, 'db', 'assessor-schema.sql'), 'utf8');
await pool.query(ddl);
console.log('[ingest] schema applied (cre.assessor_parcel). reading', FILE);
const rl = readline.createInterface({ input: fs.createReadStream(FILE), crlfDelay: Infinity });
let batch = [], n = 0, bad = 0;
for await (const line of rl) {
if (!line.trim()) continue;
let a; try { a = JSON.parse(line); } catch { bad++; continue; }
if (!a.AIN) { bad++; continue; }
batch.push(row(a));
if (batch.length >= BATCH) { await flush(batch); n += batch.length; batch = []; if (n % 50000 === 0) console.log(`[ingest] ${n.toLocaleString()} parcels…`); }
}
await flush(batch); n += batch.length;
const cnt = (await pool.query('SELECT count(*)::int c FROM assessor_parcel')).rows[0].c;
console.log(`[ingest] DONE — ${n.toLocaleString()} rows ingested (${bad} skipped) · table now ${cnt.toLocaleString()} parcels · $0 (local)`);
await pool.end();
// Rebuild the prod SQLite artifact so it can NEVER drift from the DB: every ingest -> fresh file.
// (Prod has no Postgres; data/assessor.sqlite is what /api/property-history falls back to.)
// Non-fatal: a failed export leaves the last good file in place and warns.
try {
console.log('[ingest] rebuilding data/assessor.sqlite …');
execFileSync(process.execPath, [path.join(__dirname, 'export-assessor-sqlite.js')], { stdio: 'inherit' });
} catch (e) {
console.error('[ingest] WARN — SQLite rebuild failed; prod file is now STALE:', e.message);
}
}
main().catch(e => { console.error('[ingest] FAILED:', e.message); process.exit(1); });