[object Object]

← back to Nationalrealestate

Add San Diego full (SANDAG 1.09M): authoritative ASR_ZONE->label map (data dictionary) makes coded use descriptive -> from_parcel classifies. SD commercial verified plausible (commercial+industrial mix, not Cook-skew). +commercial keyword in from_parcel pre-filter

6ad5d70aa184ef1ea72a1d7413884caf4e9f6822 · 2026-07-30 21:38:51 -0700 · steve@designerwallcoverings.com

Files touched

Diff

commit 6ad5d70aa184ef1ea72a1d7413884caf4e9f6822
Author: steve@designerwallcoverings.com <steve@designerwallcoverings.com>
Date:   Thu Jul 30 21:38:51 2026 -0700

    Add San Diego full (SANDAG 1.09M): authoritative ASR_ZONE->label map (data dictionary) makes coded use descriptive -> from_parcel classifies. SD commercial verified plausible (commercial+industrial mix, not Cook-skew). +commercial keyword in from_parcel pre-filter
---
 src/ingest/commercial/engine.ts      |  1 +
 src/ingest/commercial/from_parcel.ts |  2 +-
 src/ingest/parcels/arcgis_sales.ts   | 17 +++++++++++++++++
 src/ingest/parcels/engine.ts         |  1 +
 4 files changed, 20 insertions(+), 1 deletion(-)

diff --git a/src/ingest/commercial/engine.ts b/src/ingest/commercial/engine.ts
index 05bcc61..3822771 100644
--- a/src/ingest/commercial/engine.ts
+++ b/src/ingest/commercial/engine.ts
@@ -12,6 +12,7 @@ const ADAPTERS: Record<string, () => Promise<{ run: () => Promise<{ upserted: nu
   // 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') }),
   // NOTE: Cook(17031)/NY(36061)/Bexar-TX(48029) use CLASS CODES ('Class 5-93','C7','F1')
   // not descriptive text, so from_parcel can't classify them (verified: Cook came back
   // 100% industrial — implausible). They need a per-county class-code map (future task).
diff --git a/src/ingest/commercial/from_parcel.ts b/src/ingest/commercial/from_parcel.ts
index efd5338..8d05543 100644
--- a/src/ingest/commercial/from_parcel.ts
+++ b/src/ingest/commercial/from_parcel.ts
@@ -15,7 +15,7 @@ import { classifyCommercialByDesc } from '../../lib/commercial_types.ts';
 const digits = (v: unknown): string | null => { const t = (v == null ? '' : String(v)).replace(/\D/g, ''); return t || null; };
 
 // Broad SQL pre-filter covering the classifier's keywords — precise typing happens in JS.
-const KEYWORDS = 'office|professional|bank|savings|warehous|distribution|storage|manufactur|industrial|store|shop|supermarket|department|restaurant|cocktail|hotel|motel|hospitality|lodging|parking|studio|motion picture|television|radio|retail|wholesale|outlet|nursery|greenhouse|kennel|laundry|repair|service station|auto|recreation';
+const KEYWORDS = 'commercial|office|professional|bank|savings|warehous|distribution|storage|manufactur|industrial|store|shop|supermarket|department|restaurant|cocktail|hotel|motel|hospitality|lodging|parking|studio|motion picture|television|radio|retail|wholesale|outlet|nursery|greenhouse|kennel|laundry|repair|service station|auto|recreation';
 
 const COLS = ['county_fips', 'ain', 'address', 'city', 'zip', 'ctype', 'use_desc', 'use_class',
   'assessed_total', 'assessed_land', 'assessed_imp', 'roll_year', 'recording_date', 'sqft', 'year_built', 'units'];
diff --git a/src/ingest/parcels/arcgis_sales.ts b/src/ingest/parcels/arcgis_sales.ts
index 69d166a..01cab93 100644
--- a/src/ingest/parcels/arcgis_sales.ts
+++ b/src/ingest/parcels/arcgis_sales.ts
@@ -214,6 +214,23 @@ const SOURCES: Record<string, Source> = {
         use: clean(a.UseDescription), year: num(a.YearBuilt1), beds: num(a.Bedrooms1), baths: num(a.Bathrooms1),
         sqft: num(a.SQFTmain1), totalValue: (land || imp) ? (land || 0) + (imp || 0) : null, raw: a } as Rec; }).filter(Boolean) as Rec[],
   },
+  'san-diego-sandag': {
+    // Full San Diego County (1.09M) from SANDAG. Codes only, BUT the SanGIS data
+    // dictionary gives the authoritative ASR_ZONE map, so we translate the zone code
+    // to its descriptive label at ingest → use_desc becomes text → from_parcel classifies.
+    key: 'san-diego-sandag', label: 'San Diego County CA (SANDAG full)', cursorKey: 'san_diego_sandag', geometry: false, orderBy: 'objectid', pageSize: 2000,
+    layer: 'https://geo.sandag.org/server/rest/services/Hosted/Parcels/FeatureServer/0',
+    where: 'apn IS NOT NULL',
+    outFields: 'apn,situs_address,situs_street,situs_juris,asr_zone,asr_total',
+    fieldMap: { source_id: 'apn', address: 'situs_address', use_desc: 'asr_zone(mapped)', total_value: 'asr_total' },
+    toRecords: (fs) => fs.map(f => { const a = f.attributes; const apn = s(a.apn); if (!apn) return null;
+      // Authoritative ASR_ZONE → label (SanGIS data dictionary, verified 2026-07-30).
+      const ZONE: Record<string, string> = { '0': 'Unzoned', '1': 'Single Family Residential', '2': 'Minor Multiple Residential', '3': 'Restricted Multiple Residential', '4': 'Multiple Residential', '5': 'Restricted Commercial', '6': 'Commercial', '7': 'Industrial', '8': 'Agricultural', '9': 'Special/Misc' };
+      const use = ZONE[String(a.asr_zone)] ?? null;
+      const addr = clean(a.situs_address) || [s(a.situs_street)].filter(Boolean).join(' ') || null;
+      return { fips: '06073', apn, address: addr, city: clean(a.situs_juris), zip: null,
+        use, totalValue: num(a.asr_total), raw: a } as Rec; }).filter(Boolean) as Rec[],
+  },
   'santa-clara': {
     // Santa Clara County (San Jose) — public layer is address+APN only (no value/use),
     // maxRecordCount=2000. Parcels key on county_fips so no region-resolve needed.
diff --git a/src/ingest/parcels/engine.ts b/src/ingest/parcels/engine.ts
index 310abcc..60bf8ce 100644
--- a/src/ingest/parcels/engine.ts
+++ b/src/ingest/parcels/engine.ts
@@ -30,6 +30,7 @@ const ADAPTERS: Record<string, () => Promise<{ run: () => Promise<{ upserted: nu
   maricopa: async () => { const m = await import('./arcgis_sales.ts'); return { run: m.ingestArcgisSales('maricopa') }; },
   'la-county': async () => { const m = await import('./arcgis_sales.ts'); return { run: m.ingestArcgisSales('la-county') }; },
   'santa-clara': async () => { const m = await import('./arcgis_sales.ts'); return { run: m.ingestArcgisSales('santa-clara') }; },
+  'san-diego-sandag': async () => { const m = await import('./arcgis_sales.ts'); return { run: m.ingestArcgisSales('san-diego-sandag') }; },
   thurston: async () => { const m = await import('./arcgis_sales.ts'); return { run: m.ingestArcgisSales('thurston') }; },
   'palm-beach': async () => { const m = await import('./arcgis_sales.ts'); return { run: m.ingestArcgisSales('palm-beach') }; },
   klamath: async () => { const m = await import('./arcgis_sales.ts'); return { run: m.ingestArcgisSales('klamath') }; },

← b3ca8a1 Commercial from_parcel: add bare-'Commercial' fallback class  ·  back to Nationalrealestate  ·  Add Maricopa (Phoenix) bulk loader — free assessor st42060 f 36c7c42 →