← back to Nationalrealestate

src/ingest/parcels/maricopa_bulk.ts

70 lines

/**
 * 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;
  }
}