← back to Nationalrealestate

src/ingest/commercial/engine.ts

75 lines

/**
 * Commercial-layer ingest dispatcher: npm run ingest:commercial -- <county>
 * Counties: la (LA County assessor sqlite -> commercial_parcel). More CA counties
 * (san-diego, orange, sf, ...) register here as their adapters are added.
 */
import { pool } from '../../../db/pool.ts';

const ADAPTERS: Record<string, () => Promise<{ run: () => Promise<{ upserted: number }> }>> = {
  la: async () => ({ run: (await import('./la_assessor.ts')).ingestLaCommercial }),
  // Live ArcGIS commercial ETL (West-Coast-first; successor to the LA sqlite path).
  'la-arcgis': async () => ({ run: (await import('./arcgis_commercial.ts')).ingestArcgisCommercial('la') }),
  // Materialize commercial from already-loaded parcels (use_desc is descriptive) — $0.
  king: async () => ({ run: (await import('./from_parcel.ts')).ingestCommercialFromParcel('53033', 'King County WA (Seattle)') }),
  saltlake: async () => ({ run: (await import('./from_parcel.ts')).ingestCommercialFromParcel('49035', 'Salt Lake County UT') }),
  sandiego: async () => ({ run: (await import('./from_parcel.ts')).ingestCommercialFromParcel('06073', 'San Diego County CA') }),
  wake: async () => ({ run: (await import('./from_parcel.ts')).ingestCommercialFromParcel('37183', 'Wake County NC (Raleigh)') }),
  // Texas Comptroller state category (authoritative): F1=commercial real, F2=industrial real;
  // A/B/E=residential, C/D=vacant/ag, L=personal-property (not real estate → excluded).
  texas: async () => {
    const m = await import('./from_parcel.ts');
    const classify = (u: string) => { const c = (u || '').trim().toUpperCase(); if (c.startsWith('F2')) return 'industrial' as const; return c[0] === 'F' ? 'other' as const : null; };
    const CO: [string, string][] = [['48029', 'Bexar (San Antonio)'], ['48439', 'Tarrant (Fort Worth)']];
    return { run: async () => { let up = 0; for (const [fips, name] of CO) up += (await m.ingestCommercialFromParcelCoded(fips, `TX ${name}`, '^F[0-9]', classify)()).upserted; return { upserted: up }; } };
  },
  // NYC (Manhattan) building-class letter is authoritative (NYC DOF): O=office, K=store/retail,
  // E=warehouse+F=factory=industrial, H=hotel, G/J/L/P=other commercial; A-D/R/S/V=residential/vacant.
  nyc: async () => {
    const m = await import('./from_parcel.ts');
    const MAP: Record<string, any> = { O: 'office', K: 'retail', E: 'industrial', F: 'industrial', H: 'hospitality', G: 'other', J: 'other', L: 'other', P: 'other' };
    const classify = (u: string) => { const mm = u.match(/Class\s+([A-Za-z])/); return mm ? (MAP[mm[1].toUpperCase()] ?? null) : null; };
    const BOROUGHS: [string, string][] = [['36061', 'Manhattan'], ['36047', 'Brooklyn'], ['36081', 'Queens'], ['36005', 'Bronx'], ['36085', 'Staten Island']];
    return { run: async () => { let up = 0; for (const [fips, name] of BOROUGHS) up += (await m.ingestCommercialFromParcelCoded(fips, `NYC ${name}`, 'Class [EFGHJKLOP]', classify)()).upserted; return { upserted: up }; } };
  },
  // Cook IL: use_desc has a DESCRIPTIVE prefix ('Commercial — Class 517', 'Industrial — ...',
  // 'Residential (<7 units)'), so from_parcel classifies it directly now (commercial keyword
  // + bare-commercial fallback added post-cycle-13). NY/TX handled by the coded materializer above.
  cook: async () => ({ run: (await import('./from_parcel.ts')).ingestCommercialFromParcel('17031', 'Cook County IL (Chicago)') }),
  // FL Miami-Dade + OH Franklin: descriptive use_desc with subtypes (OFFICE/STORE/WAREHOUSE/HOTEL) → from_parcel classifies directly.
  alameda: async () => ({ run: (await import('./from_parcel.ts')).ingestCommercialFromParcel('06001', 'Alameda County CA (Oakland)') }),
  // Oregon counties use 3-letter use codes (COM=commercial, IND=industrial) — small explicit map.
  oregon: async () => {
    const m = await import('./from_parcel.ts');
    const classify = (u: string) => { const c = (u || '').trim().toUpperCase(); if (c === 'IND') return 'industrial' as const; return c === 'COM' ? 'other' as const : null; };
    const CO: [string, string][] = [['41067', 'Washington OR'], ['41051', 'Multnomah OR'], ['41005', 'Clackamas OR'], ['41047', 'Marion OR'], ['41035', 'Klamath OR']];
    return { run: async () => { let up = 0; for (const [fips, name] of CO) up += (await m.ingestCommercialFromParcelCoded(fips, name, '^(COM|IND)$', classify)()).upserted; return { upserted: up }; } };
  },
  miami: async () => ({ run: (await import('./from_parcel.ts')).ingestCommercialFromParcel('12086', 'Miami-Dade County FL') }),
  franklin: async () => ({ run: (await import('./from_parcel.ts')).ingestCommercialFromParcel('39049', 'Franklin County OH (Columbus)') }),
  // Fulton GA (Atlanta): numeric land-use codes → authoritative label map (Fulton Assessor
  // LUC PDF, data/fulton-luc.json) → classifyCommercialByDesc on the label.
  fulton: async () => {
    const m = await import('./from_parcel.ts');
    const { classifyCommercialByDesc } = await import('../../lib/commercial_types.ts');
    const { readFileSync } = await import('node:fs');
    const { join, dirname } = await import('node:path'); const { fileURLToPath } = await import('node:url');
    const MAP: Record<string, string> = JSON.parse(readFileSync(join(dirname(fileURLToPath(import.meta.url)), '../../../data/fulton-luc.json'), 'utf8'));
    const classify = (u: string) => { const label = MAP[(u || '').trim()]; return label ? classifyCommercialByDesc(label) : null; };
    return { run: m.ingestCommercialFromParcelCoded('13121', 'Fulton County GA (Atlanta)', '^(2[5-9][0-9]|[3-9][0-9][0-9])', classify) };
  },
};

async function main() {
  const which = (process.argv[2] || '').toLowerCase();
  if (!ADAPTERS[which]) {
    console.error(`usage: npm run ingest:commercial -- <${Object.keys(ADAPTERS).join('|')}>`);
    process.exit(2);
  }
  const { run } = await ADAPTERS[which]();
  const { upserted } = await run();
  console.log(`[commercial:${which}] done, ${upserted} upserted`);
  await pool.end();
}

main().catch(e => { console.error(e); process.exit(1); });