← back to La Socrata Ingester
Property drill-down (scripts/property.js): full permit/trade crew-structure per parcel
fa030eba10836a40beb168f2fe77bd25e6a19b34 · 2026-08-12 08:26:49 -0700 · steve
Assembles every permit on a parcel grouped by trade (GC building + electrical/plumbing/
HVAC sub-permits = the crew) + parcel data + timeline. Answers 'who worked on this build'
as trade structure; contractor names are the LADBS permit-detail scrape layer (next).
Files touched
Diff
commit fa030eba10836a40beb168f2fe77bd25e6a19b34
Author: steve <steve@designerwallcoverings.com>
Date: Wed Aug 12 08:26:49 2026 -0700
Property drill-down (scripts/property.js): full permit/trade crew-structure per parcel
Assembles every permit on a parcel grouped by trade (GC building + electrical/plumbing/
HVAC sub-permits = the crew) + parcel data + timeline. Answers 'who worked on this build'
as trade structure; contractor names are the LADBS permit-detail scrape layer (next).
---
scripts/property.js | 53 +++++++++++++++++++++++++++++++++++++++++++++++++++++
1 file changed, 53 insertions(+)
diff --git a/scripts/property.js b/scripts/property.js
new file mode 100644
index 0000000..5b74b81
--- /dev/null
+++ b/scripts/property.js
@@ -0,0 +1,53 @@
+// Property drill-down (READ-ONLY, $0 local). "Everything we hold for a build": the
+// parcel + its full permit history grouped by TRADE (GC building permit + the electrical/
+// plumbing/HVAC SUB-permits on the same parcel = the crew structure) + a summary.
+// Contractor NAMES per permit come from a separate LADBS permit-detail scrape layer
+// (the bulk feeds are anonymized) — this shows the trades involved and the timeline.
+//
+// Usage: node scripts/property.js "<address substring>" | node scripts/property.js <APN>
+import { q, pool } from '../src/db.js';
+const money = v => v == null || Number(v) === 0 ? '—' : '$' + Math.round(Number(v)).toLocaleString();
+const TRADE = { 'pi9x-tg5x': 'BUILDING (GC)', 'dyxf-7hc4': 'BUILDING (GC, 2010-19)', 'e67z-kt2n': 'BUILDING (GC, pre-2010)', 'ysqd-apz7': 'ELECTRICAL', '67is-svtd': 'MECH/PLUMB' };
+
+async function main() {
+ const arg = process.argv.slice(2).join(' ').trim();
+ if (!arg) { console.error('usage: node scripts/property.js "<address>" | <APN>'); process.exit(1); }
+ const isApn = /^\d{7,10}$/.test(arg.replace(/[^0-9]/g, '')) && !/\s/.test(arg);
+
+ // resolve to an APN
+ const apnRow = isApn
+ ? { apn: arg.replace(/[^0-9]/g, '') }
+ : (await q(`SELECT apn, primary_address FROM la_building_permits_raw WHERE primary_address ILIKE $1 AND apn IS NOT NULL ORDER BY issue_date DESC LIMIT 1`, ['%' + arg + '%'])).rows[0];
+ if (!apnRow?.apn) { console.error('no property found for: ' + arg); process.exit(1); }
+ const apn = apnRow.apn;
+
+ // parcel (assessor 2025)
+ const parcel = (await q(`SELECT property_location, situs_zip5, use_type, year_built, sqft_main, total_value, land_value, imp_value FROM la_assessor_parcels_raw WHERE ain=$1 AND roll_year='2025' LIMIT 1`, [apn])).rows[0];
+ const addr = parcel?.property_location || apnRow.primary_address || '(address unknown)';
+
+ // all permits on this parcel
+ const permits = (await q(`
+ SELECT dataset_id, permit_nbr, to_char(issue_date,'YYYY-MM-DD') issued, permit_type, permit_sub_type,
+ valuation, status_desc, use_desc, work_desc
+ FROM la_building_permits_raw WHERE apn=$1 ORDER BY issue_date NULLS FIRST`, [apn])).rows;
+
+ console.log(`\n=== PROPERTY DRILL-DOWN — ${addr} ===`);
+ console.log(`APN ${apn}${parcel ? ` · ${parcel.situs_zip5 || ''} · ${parcel.use_type || ''}` : ''}`);
+ if (parcel) console.log(`Parcel: assessed ${money(parcel.total_value)} (land ${money(parcel.land_value)} / improvements ${money(parcel.imp_value)}) · built ${parcel.year_built || '—'} · ${parcel.sqft_main ? Number(parcel.sqft_main).toLocaleString() + ' sqft' : '—'}`);
+
+ // crew structure — permit count by trade
+ const byTrade = {};
+ for (const p of permits) { const t = TRADE[p.dataset_id] || p.dataset_id; (byTrade[t] ||= 0); byTrade[t]++; }
+ console.log(`\nCREW / TRADES on this parcel (${permits.length} permits total):`);
+ for (const [t, n] of Object.entries(byTrade).sort((a, b) => b[1] - a[1])) console.log(` ${t.padEnd(26)} ${n} permit${n > 1 ? 's' : ''}`);
+
+ console.log(`\nPERMIT HISTORY (timeline):`);
+ for (const p of permits) {
+ const t = (TRADE[p.dataset_id] || p.dataset_id).replace(/ \(.*/, '');
+ console.log(` ${p.issued || '(n/a) '} ${t.padEnd(14)} ${(p.permit_type || '').padEnd(18)} ${money(p.valuation).padStart(12)} ${p.status_desc || ''}`);
+ }
+ const bldgVal = permits.filter(p => /BUILDING/.test(TRADE[p.dataset_id] || '')).reduce((s, p) => s + Number(p.valuation || 0), 0);
+ console.log(`\nSummary: ${permits.length} permits, ${Object.keys(byTrade).length} trade categories, ${money(bldgVal)} total building valuation.`);
+ console.log(`Contractor names per permit: available via the LADBS permit-detail layer (permit_nbr -> detail). Not in the bulk feed.\n`);
+}
+main().catch(e => { console.error('property error:', e.message); process.exitCode = 1; }).finally(() => pool.end());
← 012231a enrich: domain-guessing pipeline (scripts/enrich-domain.js)
·
back to La Socrata Ingester
·
Named build crew (scripts/permit-crew.js) — the linkage laye 1cd9469 →