[object Object]

← back to Nationalrealestate

Add Maricopa (Phoenix) bulk loader — free assessor st42060 fixed-width extract carries PROPERTY USE CODE (public ArcGIS had none). Maps authoritative PUC 2-digit prefix->descriptive label (PUC manual) so from_parcel classifies. Proven on sample (PUC 01->Single Family Residential); unknown prefixes safely skipped

36c7c42c43916667970e1de5e4629b7954cd4a1d · 2026-07-30 22:54:00 -0700 · steve@designerwallcoverings.com

Files touched

Diff

commit 36c7c42c43916667970e1de5e4629b7954cd4a1d
Author: steve@designerwallcoverings.com <steve@designerwallcoverings.com>
Date:   Thu Jul 30 22:54:00 2026 -0700

    Add Maricopa (Phoenix) bulk loader — free assessor st42060 fixed-width extract carries PROPERTY USE CODE (public ArcGIS had none). Maps authoritative PUC 2-digit prefix->descriptive label (PUC manual) so from_parcel classifies. Proven on sample (PUC 01->Single Family Residential); unknown prefixes safely skipped
---
 src/ingest/parcels/engine.ts        |  1 +
 src/ingest/parcels/maricopa_bulk.ts | 69 +++++++++++++++++++++++++++++++++++++
 2 files changed, 70 insertions(+)

diff --git a/src/ingest/parcels/engine.ts b/src/ingest/parcels/engine.ts
index 60bf8ce..b782bf2 100644
--- a/src/ingest/parcels/engine.ts
+++ b/src/ingest/parcels/engine.ts
@@ -10,6 +10,7 @@ const ADAPTERS: Record<string, () => Promise<{ run: () => Promise<{ upserted: nu
   nyc: async () => ({ run: (await import('./nyc_pluto.ts')).ingestNycPluto }),
   'nyc-acris': async () => ({ run: (await import('./nyc_acris.ts')).ingestNycAcris }),
   king: async () => ({ run: (await import('./king_wa.ts')).ingestKing }),
+  'maricopa-bulk': async () => { const m = await import('./maricopa_bulk.ts'); const p = process.argv[3]; return { run: () => m.ingestMaricopaBulk(p) }; },
   cook: async () => ({ run: (await import('./cook_il.ts')).ingestCook }),
   franklin: async () => ({ run: (await import('./franklin_oh.ts')).ingestFranklin }),
   miami: async () => ({ run: (await import('./miami_dade.ts')).ingestMiami }),
diff --git a/src/ingest/parcels/maricopa_bulk.ts b/src/ingest/parcels/maricopa_bulk.ts
new file mode 100644
index 0000000..b9b5be0
--- /dev/null
+++ b/src/ingest/parcels/maricopa_bulk.ts
@@ -0,0 +1,69 @@
+/**
+ * Maricopa County (Phoenix, 04013) loader from the FREE assessor bulk file (st42060,
+ * launched Mar 2026, no fees). The public ArcGIS has no property-use, but this
+ * fixed-width extract carries the PROPERTY USE CODE. We map the PUC's authoritative
+ * 2-digit prefix → its descriptive label (Maricopa PUC manual), so use_desc becomes
+ * descriptive text and from_parcel classifies it (same SD ASR_ZONE trick).
+ *
+ * Fixed-width layout (st42060, 1-indexed): RECORD TYPE 1-3 (only '100' rows carry
+ * PUC+situs), APN 4-12, PROPERTY USE CODE 14-17, SITUS STREET NUM 52-57, DIR 58-59,
+ * NAME 60-99, TYPE 100-111, CITY 133-162, ZIP 163-171, LPV FULL CASH VALUE 459-470.
+ *
+ *   npm run ingest:parcels -- maricopa-bulk /path/to/st42060.dat
+ */
+import { readFileSync } from 'node:fs';
+import { upsertParcels, normAddress, type ParcelUpsertRow } from './upsert.ts';
+import { openRun, closeRun } from '../run.ts';
+
+// Authoritative Maricopa PUC 2-digit prefix → descriptive label (PUC manual). The label
+// text carries the keyword classifyCommercialByDesc matches; residential/vacant/ag → the
+// classifier returns null (not commercial). Prefixes we don't have a label for → skipped.
+const PUC: Record<string, string> = {
+  '00': 'Vacant Land', '01': 'Single Family Residential', '02': 'Residential Recreation',
+  '03': 'Multiple Family Residential', '04': 'High Density Agriculture', '05': 'Water Utilities',
+  '06': 'Railroad & Utilities', '07': 'Condominium Residential', '08': 'Mobile Homes',
+  '09': 'Salvage & Misc Commercial', '10': 'Miscellaneous Commercial', '11': 'Convenience Retail Store',
+  '12': 'Department Store', '13': 'Shopping Center', '14': 'Office Building', '15': 'Residential over 5 Acres',
+  '16': 'Bank & Financial', '17': 'Service Station & Auto', '18': 'Auto Sales', '19': 'Nursing Home Medical',
+  '20': 'Restaurant', '21': 'Medical Facility', '22': 'Race Track & Air Field', '23': 'Cemetery Mortuary',
+  '24': 'Golf Course', '25': 'Amusement Recreation', '26': 'Parking Facility', '27': 'Club & Health Facility',
+  '28': 'Improvements', '29': 'Private School', '30': 'Industrial Park',
+};
+
+const fw = (line: string, start1: number, len: number): string => line.substr(start1 - 1, len).trim();
+const digits = (v: string): string | null => { const t = (v || '').replace(/\D/g, ''); return t || null; };
+const num = (v: string): number | null => { const x = Number((v || '').replace(/[^0-9.]/g, '')); return Number.isFinite(x) && x > 0 ? x : null; };
+
+export async function ingestMaricopaBulk(datPath: string) {
+  const runId = await openRun('maricopa_bulk', datPath);
+  try {
+    const text = readFileSync(datPath, 'latin1');
+    const lines = text.split(/\r?\n/);
+    const rows: ParcelUpsertRow[] = [];
+    let skipped = 0;
+    for (const line of lines) {
+      if (line.substr(0, 3) !== '100') { skipped++; continue; } // only master rows carry PUC+situs
+      const apn = digits(fw(line, 4, 9)); if (!apn) { skipped++; continue; }
+      const puc = fw(line, 14, 4);
+      const label = PUC[puc.slice(0, 2)] ?? null; // authoritative prefix → label; unknown → null (safe)
+      const addr = [fw(line, 52, 6), fw(line, 58, 2), fw(line, 60, 40), fw(line, 100, 12)].filter(Boolean).join(' ').replace(/\s+/g, ' ').trim() || null;
+      const city = fw(line, 133, 30) || null;
+      const zip = (fw(line, 163, 9) || '').slice(0, 5) || null;
+      const value = num(fw(line, 459, 12));
+      rows.push({
+        county_fips: '04013', source_id: apn, address: addr, norm_address: addr ? normAddress(addr) : null,
+        city, zip, lat: null, lng: null, year_built: null, sqft: null, beds: null, baths: null,
+        units: null, use_desc: label, land_value: null, improvement_value: null, total_value: value,
+        tax_year: null, owner_name: null, zoning: null, last_sale_date: null, last_sale_price: null,
+        extra: JSON.stringify({ puc }), sourceKey: 'maricopa_bulk', rawSource: null,
+      });
+    }
+    const up = await upsertParcels(rows);
+    await closeRun(runId, 'ok', { upserted: up, skipped, notes: `Maricopa bulk st42060: ${up} parcels (${skipped} non-master/blank)` });
+    console.log(`[maricopa-bulk] done: ${up} parcels upserted (${skipped} skipped)`);
+    return { upserted: up };
+  } catch (e: any) {
+    await closeRun(runId, 'failed', { notes: e.message });
+    throw e;
+  }
+}

← 6ad5d70 Add San Diego full (SANDAG 1.09M): authoritative ASR_ZONE->l  ·  back to Nationalrealestate  ·  Add Wake County NC commercial (3764, Raleigh) via safe word- 275ff1e →