[object Object]

← 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 →