[object Object]

← back to Bubbesblock

Add home-history module with LA County parcel + LADBS permit lookup

698091f415f1b39c59c39f71634b0895fc59c1e8 · 2026-06-19 16:12:06 -0700 · SteveStudio2

- lib/home-history.js: CommonJS module exporting getHomeHistory(address)
  pulls Tier A data from LA County GIS ArcGIS REST (free, no auth) and
  LADBS Socrata permit datasets; JSON cache at data/home-history.json;
  resilient (never throws, returns honest unavailable stub on no data)
- data/home-history.json: pre-seeded for 13 BubbesBlock lead addresses;
  2 with real assessor data (18406 Bessemer St + 18700 Ventura Blvd),
  11 honest unavailable stubs (fictionally-numbered addresses not in the
  LA County public parcel roll)
- scripts/seed-home-history.js: one-shot cache warmer

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

Files touched

Diff

commit 698091f415f1b39c59c39f71634b0895fc59c1e8
Author: SteveStudio2 <stevestudio2@SteveStacStudio.lan>
Date:   Fri Jun 19 16:12:06 2026 -0700

    Add home-history module with LA County parcel + LADBS permit lookup
    
    - lib/home-history.js: CommonJS module exporting getHomeHistory(address)
      pulls Tier A data from LA County GIS ArcGIS REST (free, no auth) and
      LADBS Socrata permit datasets; JSON cache at data/home-history.json;
      resilient (never throws, returns honest unavailable stub on no data)
    - data/home-history.json: pre-seeded for 13 BubbesBlock lead addresses;
      2 with real assessor data (18406 Bessemer St + 18700 Ventura Blvd),
      11 honest unavailable stubs (fictionally-numbered addresses not in the
      LA County public parcel roll)
    - scripts/seed-home-history.js: one-shot cache warmer
    
    Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
---
 data/home-history.json       | 450 +++++++++++++++++++++++++++++++++++++++++++
 lib/home-history.js          | 397 ++++++++++++++++++++++++++++++++++++++
 scripts/seed-home-history.js |  45 +++++
 3 files changed, 892 insertions(+)

diff --git a/data/home-history.json b/data/home-history.json
new file mode 100644
index 0000000..3eab380
--- /dev/null
+++ b/data/home-history.json
@@ -0,0 +1,450 @@
+{
+  "18406 BESSEMER ST TARZANA CA 91406": {
+    "address": "18406 Bessemer Street, Tarzana CA 91406",
+    "normalized": "18406 BESSEMER ST TARZANA CA 91406",
+    "apn": "2124-013-005",
+    "builtYear": 1950,
+    "lastSold": {
+      "date": "~2024",
+      "price": null,
+      "note": "Estimated from Prop 13 reassessment base year. Actual sale date/price requires LA County Registrar-Recorder deed research (no free online index — in-person or CPRA request required)."
+    },
+    "beds": 3,
+    "baths": 2,
+    "sqft": 1418,
+    "assessedValue": 1009800,
+    "useDescription": "Single",
+    "residents": [],
+    "permits": [
+      {
+        "permitNumber": "19016-90000-06622",
+        "type": "Bldg-Alter/Repair",
+        "subType": "1 or 2 Family Dwelling",
+        "issueDate": "2019-03-08",
+        "status": "Permit Expired",
+        "valuation": 5000,
+        "description": "Reroof with 25 sqrs COMP SHINGLE roofing. Existing solid sheathing.Re-roof with Class A or B material weighing less than",
+        "dataset": "2010-19"
+      },
+      {
+        "permitNumber": "06016-20000-03752",
+        "type": "Bldg-Alter/Repair",
+        "subType": "1 or 2 Family Dwelling",
+        "issueDate": "2006-02-28",
+        "status": "Permit Finaled",
+        "valuation": 5000,
+        "description": "T/O (E) ROOF, INSTALL COMPOSITION SHINGLES CLASS \"A\" 25SQS.  SMOKE DETECTORS REQUIRED.",
+        "dataset": "pre-2010"
+      }
+    ],
+    "summary": "18406 BESSEMER ST LOS ANGELES CA 91335: Single. Built 1950. 1,418 sq ft. 3 bed / 2 bath. Last transfer approx. ~2024 (Prop 13 base year reassessment). Total assessed value: $1,009,800 (2025). 2 building permit(s) on LADBS record; most recent: Bldg-Alter/Repair (2019-03-08). APN: 2124-013-005. Owner name, sale price, and resident history are not available from free public sources.",
+    "source": "LA County GIS ArcGIS REST (parcel) + LADBS Socrata (permits)",
+    "sourceUrl": "https://portal.assessor.lacounty.gov/parceldetail/2124013005",
+    "tier": "A",
+    "retrievedAt": "2026-06-19T23:10:27.824Z",
+    "_raw": {
+      "situsFullAddress": "18406 BESSEMER ST LOS ANGELES CA 91335",
+      "landValue": 660348,
+      "improvementValue": 349452,
+      "landBaseYear": "2024",
+      "improvementBaseYear": "2024",
+      "rollYear": "2025",
+      "homeownerExemption": false,
+      "effectiveYear": "1953",
+      "lat": 34.18082222,
+      "lng": -118.53391686
+    }
+  },
+  "18722 YOLANDA AVE TARZANA CA 91356": {
+    "address": "18722 Yolanda Ave, Tarzana CA 91356",
+    "normalized": "18722 YOLANDA AVE TARZANA CA 91356",
+    "builtYear": null,
+    "lastSold": null,
+    "beds": null,
+    "baths": null,
+    "sqft": null,
+    "residents": [],
+    "permits": [],
+    "summary": "No parcel record found in LA County public GIS layer for this address. The address may be outside LA County, use a different number format, or may not be indexed in the current roll.",
+    "source": "unavailable",
+    "sourceUrl": "https://public.gis.lacounty.gov/public/rest/services/LACounty_Cache/LACounty_Parcel/MapServer/0",
+    "tier": "A",
+    "retrievedAt": "2026-06-19T23:10:31.370Z"
+  },
+  "18450 BESSEMER ST TARZANA CA 91406": {
+    "address": "18450 Bessemer St, Tarzana CA 91406",
+    "normalized": "18450 BESSEMER ST TARZANA CA 91406",
+    "builtYear": null,
+    "lastSold": null,
+    "beds": null,
+    "baths": null,
+    "sqft": null,
+    "residents": [],
+    "permits": [],
+    "summary": "No parcel record found in LA County public GIS layer for this address. The address may be outside LA County, use a different number format, or may not be indexed in the current roll.",
+    "source": "unavailable",
+    "sourceUrl": "https://public.gis.lacounty.gov/public/rest/services/LACounty_Cache/LACounty_Parcel/MapServer/0",
+    "tier": "A",
+    "retrievedAt": "2026-06-19T23:10:33.943Z"
+  },
+  "5210 LINDLEY AVE TARZANA CA 91356": {
+    "address": "5210 Lindley Ave, Tarzana CA 91356",
+    "normalized": "5210 LINDLEY AVE TARZANA CA 91356",
+    "builtYear": null,
+    "lastSold": null,
+    "beds": null,
+    "baths": null,
+    "sqft": null,
+    "residents": [],
+    "permits": [],
+    "summary": "No parcel record found in LA County public GIS layer for this address. The address may be outside LA County, use a different number format, or may not be indexed in the current roll.",
+    "source": "unavailable",
+    "sourceUrl": "https://public.gis.lacounty.gov/public/rest/services/LACounty_Cache/LACounty_Parcel/MapServer/0",
+    "tier": "A",
+    "retrievedAt": "2026-06-19T23:10:36.450Z"
+  },
+  "19010 ETIWANDA AVE TARZANA CA 91356": {
+    "address": "19010 Etiwanda Ave, Tarzana CA 91356",
+    "normalized": "19010 ETIWANDA AVE TARZANA CA 91356",
+    "builtYear": null,
+    "lastSold": null,
+    "beds": null,
+    "baths": null,
+    "sqft": null,
+    "residents": [],
+    "permits": [],
+    "summary": "No parcel record found in LA County public GIS layer for this address. The address may be outside LA County, use a different number format, or may not be indexed in the current roll.",
+    "source": "unavailable",
+    "sourceUrl": "https://public.gis.lacounty.gov/public/rest/services/LACounty_Cache/LACounty_Parcel/MapServer/0",
+    "tier": "A",
+    "retrievedAt": "2026-06-19T23:10:39.256Z"
+  },
+  "18633 WELLS DR TARZANA CA 91356": {
+    "address": "18633 Wells Dr, Tarzana CA 91356",
+    "normalized": "18633 WELLS DR TARZANA CA 91356",
+    "builtYear": null,
+    "lastSold": null,
+    "beds": null,
+    "baths": null,
+    "sqft": null,
+    "residents": [],
+    "permits": [],
+    "summary": "No parcel record found in LA County public GIS layer for this address. The address may be outside LA County, use a different number format, or may not be indexed in the current roll.",
+    "source": "unavailable",
+    "sourceUrl": "https://public.gis.lacounty.gov/public/rest/services/LACounty_Cache/LACounty_Parcel/MapServer/0",
+    "tier": "A",
+    "retrievedAt": "2026-06-19T23:10:42.428Z"
+  },
+  "18700 VENTURA BLVD TARZANA CA 91356": {
+    "address": "18700 Ventura Blvd, Tarzana CA 91356",
+    "normalized": "18700 VENTURA BLVD TARZANA CA 91356",
+    "apn": "2161-026-005",
+    "builtYear": 2009,
+    "lastSold": {
+      "date": "~2009",
+      "price": null,
+      "note": "Estimated from Prop 13 reassessment base year. Actual sale date/price requires LA County Registrar-Recorder deed research (no free online index — in-person or CPRA request required)."
+    },
+    "beds": null,
+    "baths": null,
+    "sqft": 108543,
+    "assessedValue": 59597029,
+    "useDescription": "Store Combination",
+    "residents": [],
+    "permits": [
+      {
+        "permitNumber": "25016-20000-16309",
+        "type": "Bldg-Alter/Repair",
+        "subType": "Commercial",
+        "issueDate": "2025-06-19",
+        "status": "Permit Finaled",
+        "valuation": 23000,
+        "description": "eplan: defered submital for racking anchorage connection",
+        "dataset": "2020+"
+      },
+      {
+        "permitNumber": "24016-10000-25499",
+        "type": "Bldg-Alter/Repair",
+        "subType": "Commercial",
+        "issueDate": "2025-02-19",
+        "status": "CofO Issued",
+        "valuation": 735000,
+        "description": "eplan: CHANGE OF USE OF TO COMBINE (E) RESTURANT AND RETAIL  TO BIGGER RETAIL SPACE AND NEW ROOF TOP EQUIPMENT",
+        "dataset": "2020+"
+      },
+      {
+        "permitNumber": "24048-10000-02060",
+        "type": "Sign",
+        "subType": "Onsite",
+        "issueDate": "2024-12-02",
+        "status": "Permit Finaled",
+        "valuation": 5000,
+        "description": "2'-6\" x 19'-11/16\" (47.64 S.F.) Illuminated channel letters Wall Sign 15'-3\" from bottom of sign to grade.",
+        "dataset": "2020+"
+      },
+      {
+        "permitNumber": "24048-10000-01411",
+        "type": "Sign",
+        "subType": "Onsite",
+        "issueDate": "2024-10-02",
+        "status": "Permit Finaled",
+        "valuation": 5000,
+        "description": "ONE ILLUMINATED WALL SIGN  4'H X 9' 3 3/8\" W @ 37.16 SQ FT  12' GRADE TO BOTTOM OF SIGN.",
+        "dataset": "2020+"
+      },
+      {
+        "permitNumber": "24016-10000-01154",
+        "type": "Bldg-Alter/Repair",
+        "subType": "Commercial",
+        "issueDate": "2024-06-20",
+        "status": "Permit Finaled",
+        "valuation": 350000,
+        "description": "***eplan*** Interior Tenant Improvement (T.I.) to (e) retail space for new \"Nordstrom Rack\", scope of work to include pa",
+        "dataset": "2020+"
+      },
+      {
+        "permitNumber": "19048-20000-02728",
+        "type": "Sign",
+        "subType": "Onsite",
+        "issueDate": "2021-06-30",
+        "status": "Issued",
+        "valuation": 2500,
+        "description": "INSTALL ONE NEW ONSITE ILLUMINATED CHANNEL LETTERS WALL SIGN @ 2'10\" X 21'",
+        "dataset": "2020+"
+      },
+      {
+        "permitNumber": "20016-10000-30466",
+        "type": "Bldg-Alter/Repair",
+        "subType": "Commercial",
+        "issueDate": "2021-02-26",
+        "status": "CofO Issued",
+        "valuation": 166000,
+        "description": "CHANGE TO USE FROM GYM TO MEDICAL CLINIC TI: 4618 SQ FT",
+        "dataset": "2020+"
+      },
+      {
+        "permitNumber": "20016-20000-06606",
+        "type": "Bldg-Alter/Repair",
+        "subType": "Commercial",
+        "issueDate": "2020-03-05",
+        "status": "Issued",
+        "valuation": 15000,
+        "description": "PROPOSED NONSTRUCT INT REMODEL AT EX DRESSING RM ENTRANCE WITHIN TJ MAX STORE, PER PLAN.",
+        "dataset": "2020+"
+      },
+      {
+        "permitNumber": "19016-20000-40653",
+        "type": "Bldg-Alter/Repair",
+        "subType": "Commercial",
+        "issueDate": "2019-12-20",
+        "status": "Permit Finaled",
+        "valuation": 600,
+        "description": "REMOVE AN INTERIOR NON BEARING WALL IN THE (E) RETAIL SPACE.",
+        "dataset": "2010-19"
+      },
+      {
+        "permitNumber": "18016-30000-12111",
+        "type": "Bldg-Alter/Repair",
+        "subType": "Commercial",
+        "issueDate": "2018-12-28",
+        "status": "CofO Issued",
+        "valuation": 265785,
+        "description": "CHANGE OF USE FROM  RETAIL TO BANK FOR TENANT SPACE 110",
+        "dataset": "2010-19"
+      },
+      {
+        "permitNumber": "17048-20000-00615",
+        "type": "Sign",
+        "subType": "Onsite",
+        "issueDate": "2017-03-14",
+        "status": "Issued",
+        "valuation": 4200,
+        "description": "INSTALL (N) 1'-3\" X 16'-6\" ILLUMINATED ONSITE CHANNEL LETTER WALL SIGN (\"BLUE JAM CAFE\")",
+        "dataset": "2010-19"
+      },
+      {
+        "permitNumber": "16016-30000-25524",
+        "type": "Bldg-Alter/Repair",
+        "subType": "Commercial",
+        "issueDate": "2016-12-19",
+        "status": "Permit Finaled",
+        "valuation": 60000,
+        "description": "TENANT IMPROVEMENT TO (E) RESTAURANT. NO CHANGE OF USE, OCCUPANCY OR PARKING REQUIREMENTS.",
+        "dataset": "2010-19"
+      },
+      {
+        "permitNumber": "10014-10006-03347",
+        "type": "Bldg-Alter/Repair",
+        "subType": "Apartment",
+        "issueDate": "2016-09-27",
+        "status": "Permit Finaled",
+        "valuation": 0,
+        "description": "DEPARTMENT ERROR. NO FEE.THIS PERMIT IS TO CORRECT THE STRUCTURAL INVENTORY UNDER PERMITS 10014-10000-03347, 10014-10004",
+        "dataset": "2010-19"
+      },
+      {
+        "permitNumber": "15048-10000-02066",
+        "type": "Sign",
+        "subType": "Onsite",
+        "issueDate": "2015-09-01",
+        "status": "Permit Finaled",
+        "valuation": 4698,
+        "description": "(3) DOUBLE -SIDED  ILLUMINATIED DIRECTIONAL BLADE (projecting) SIGNS: (1) 28\" x 24\" [4.67 SF, A3] AND  (2) 42\" x 57.5\" [",
+        "dataset": "2010-19"
+      },
+      {
+        "permitNumber": "15048-20000-00619",
+        "type": "Sign",
+        "subType": "Onsite",
+        "issueDate": "2015-07-29",
+        "status": "Permit Finaled",
+        "valuation": 8869,
+        "description": "Install 7 channel letters wall signs. (2) illuminated: [A4] 18.4' X 30\" READS <WELCOME> , [A1] 99\" x 27\" READS  <LOGO & ",
+        "dataset": "2010-19"
+      },
+      {
+        "permitNumber": "15048-20000-00923",
+        "type": "Sign",
+        "subType": "Onsite",
+        "issueDate": "2015-04-28",
+        "status": "Permit Finaled",
+        "valuation": 1000,
+        "description": "INSTALL (1) ILLUMINATED CHANNEL LETTER WALL SIGN (65 1/4\" X 7\") READING \"MAC\"",
+        "dataset": "2010-19"
+      },
+      {
+        "permitNumber": "10014-10005-03347",
+        "type": "Bldg-Alter/Repair",
+        "subType": "Commercial",
+        "issueDate": "2015-04-23",
+        "status": "Permit Finaled",
+        "valuation": 501,
+        "description": "TO CHANGE THE TYPE OF CONSTRUCTION FOR THE 15 UNIT TOWNHOUSES FROM TYPE V-1hr TO TYPE V-N WITH NFPA-13 AUTOMATIC FIRE SP",
+        "dataset": "2010-19"
+      },
+      {
+        "permitNumber": "10014-10004-03347",
+        "type": "Bldg-Alter/Repair",
+        "subType": "Commercial",
+        "issueDate": "2014-11-21",
+        "status": "Permit Finaled",
+        "valuation": 501,
+        "description": "THIS PERMIT IS TO REVISE ACCESSIBLE PARKING LAYOUT AND STORAGE SPACE LAYOUT BY CONVERTING ONE ACCESSIBLE PARKING STALL T",
+        "dataset": "2010-19"
+      }
+    ],
+    "summary": "18700 VENTURA BLVD LOS ANGELES CA 91356: Store Combination. Built 2009. 108,543 sq ft. Last transfer approx. ~2009 (Prop 13 base year reassessment). Total assessed value: $59,597,029 (2025). 18 building permit(s) on LADBS record; most recent: Bldg-Alter/Repair (2025-06-19). APN: 2161-026-005. Owner name, sale price, and resident history are not available from free public sources.",
+    "source": "LA County GIS ArcGIS REST (parcel) + LADBS Socrata (permits)",
+    "sourceUrl": "https://portal.assessor.lacounty.gov/parceldetail/2161026005",
+    "tier": "A",
+    "retrievedAt": "2026-06-19T23:10:44.868Z",
+    "_raw": {
+      "situsFullAddress": "18700 VENTURA BLVD LOS ANGELES CA 91356",
+      "landValue": 21428637,
+      "improvementValue": 38168392,
+      "landBaseYear": "2009",
+      "improvementBaseYear": "2009",
+      "rollYear": "2025",
+      "homeownerExemption": false,
+      "effectiveYear": "2010",
+      "lat": 34.17022911,
+      "lng": -118.54123349
+    }
+  },
+  "18211 HART ST TARZANA CA 91335": {
+    "address": "18211 Hart St, Tarzana CA 91335",
+    "normalized": "18211 HART ST TARZANA CA 91335",
+    "builtYear": null,
+    "lastSold": null,
+    "beds": null,
+    "baths": null,
+    "sqft": null,
+    "residents": [],
+    "permits": [],
+    "summary": "No parcel record found in LA County public GIS layer for this address. The address may be outside LA County, use a different number format, or may not be indexed in the current roll.",
+    "source": "unavailable",
+    "sourceUrl": "https://public.gis.lacounty.gov/public/rest/services/LACounty_Cache/LACounty_Parcel/MapServer/0",
+    "tier": "A",
+    "retrievedAt": "2026-06-19T23:10:47.422Z"
+  },
+  "18840 CREBS AVE TARZANA CA 91356": {
+    "address": "18840 Crebs Ave, Tarzana CA 91356",
+    "normalized": "18840 CREBS AVE TARZANA CA 91356",
+    "builtYear": null,
+    "lastSold": null,
+    "beds": null,
+    "baths": null,
+    "sqft": null,
+    "residents": [],
+    "permits": [],
+    "summary": "No parcel record found in LA County public GIS layer for this address. The address may be outside LA County, use a different number format, or may not be indexed in the current roll.",
+    "source": "unavailable",
+    "sourceUrl": "https://public.gis.lacounty.gov/public/rest/services/LACounty_Cache/LACounty_Parcel/MapServer/0",
+    "tier": "A",
+    "retrievedAt": "2026-06-19T23:10:49.854Z"
+  },
+  "18509 CLARK ST TARZANA CA 91356": {
+    "address": "18509 Clark St, Tarzana CA 91356",
+    "normalized": "18509 CLARK ST TARZANA CA 91356",
+    "builtYear": null,
+    "lastSold": null,
+    "beds": null,
+    "baths": null,
+    "sqft": null,
+    "residents": [],
+    "permits": [],
+    "summary": "No parcel record found in LA County public GIS layer for this address. The address may be outside LA County, use a different number format, or may not be indexed in the current roll.",
+    "source": "unavailable",
+    "sourceUrl": "https://public.gis.lacounty.gov/public/rest/services/LACounty_Cache/LACounty_Parcel/MapServer/0",
+    "tier": "A",
+    "retrievedAt": "2026-06-19T23:10:52.266Z"
+  },
+  "18320 VANALDEN AVE TARZANA CA 91356": {
+    "address": "18320 Vanalden Ave, Tarzana CA 91356",
+    "normalized": "18320 VANALDEN AVE TARZANA CA 91356",
+    "builtYear": null,
+    "lastSold": null,
+    "beds": null,
+    "baths": null,
+    "sqft": null,
+    "residents": [],
+    "permits": [],
+    "summary": "No parcel record found in LA County public GIS layer for this address. The address may be outside LA County, use a different number format, or may not be indexed in the current roll.",
+    "source": "unavailable",
+    "sourceUrl": "https://public.gis.lacounty.gov/public/rest/services/LACounty_Cache/LACounty_Parcel/MapServer/0",
+    "tier": "A",
+    "retrievedAt": "2026-06-19T23:10:54.685Z"
+  },
+  "18118 OSO AVE TARZANA CA 91335": {
+    "address": "18118 Oso Ave, Tarzana CA 91335",
+    "normalized": "18118 OSO AVE TARZANA CA 91335",
+    "builtYear": null,
+    "lastSold": null,
+    "beds": null,
+    "baths": null,
+    "sqft": null,
+    "residents": [],
+    "permits": [],
+    "summary": "No parcel record found in LA County public GIS layer for this address. The address may be outside LA County, use a different number format, or may not be indexed in the current roll.",
+    "source": "unavailable",
+    "sourceUrl": "https://public.gis.lacounty.gov/public/rest/services/LACounty_Cache/LACounty_Parcel/MapServer/0",
+    "tier": "A",
+    "retrievedAt": "2026-06-19T23:10:57.102Z"
+  },
+  "18555 RESEDA BLVD TARZANA CA 91335": {
+    "address": "18555 Reseda Blvd, Tarzana CA 91335",
+    "normalized": "18555 RESEDA BLVD TARZANA CA 91335",
+    "builtYear": null,
+    "lastSold": null,
+    "beds": null,
+    "baths": null,
+    "sqft": null,
+    "residents": [],
+    "permits": [],
+    "summary": "No parcel record found in LA County public GIS layer for this address. The address may be outside LA County, use a different number format, or may not be indexed in the current roll.",
+    "source": "unavailable",
+    "sourceUrl": "https://public.gis.lacounty.gov/public/rest/services/LACounty_Cache/LACounty_Parcel/MapServer/0",
+    "tier": "A",
+    "retrievedAt": "2026-06-19T23:10:57.706Z"
+  }
+}
\ No newline at end of file
diff --git a/lib/home-history.js b/lib/home-history.js
new file mode 100644
index 0000000..ac8d371
--- /dev/null
+++ b/lib/home-history.js
@@ -0,0 +1,397 @@
+/**
+ * home-history.js — BubbesBlock home-history lookup module
+ *
+ * Exports: async function getHomeHistory(address) -> NormalizedHomeHistory
+ *
+ * Data sources (all free, no auth):
+ *   Tier A — LA County GIS ArcGIS REST parcel layer (public.gis.lacounty.gov)
+ *             Fields: year built, sqft, beds/baths, assessed value, APN
+ *   Tier A — LADBS Socrata building-permit datasets (data.lacity.org)
+ *             Datasets: pi9x-tg5x (2020+), dyxf-7hc4 (2010-19), e67z-kt2n (<2010)
+ *
+ * What is NOT available from free sources:
+ *   - Owner names (intentionally redacted from public GIS layer per Gov Code §6254.21)
+ *   - Sale price / last-sold date (Registrar-Recorder — no free online index)
+ *   - Resident history (Census 72-yr embargo; city directories not digitized/API)
+ *
+ * Cache: data/home-history.json (object keyed by normalized address)
+ * Source tier tagging follows stayclaim schema conventions.
+ */
+
+'use strict';
+
+const fs   = require('fs');
+const path = require('path');
+const https = require('https');
+
+// ---------------------------------------------------------------------------
+// Paths
+// ---------------------------------------------------------------------------
+const CACHE_PATH = path.join(__dirname, '..', 'data', 'home-history.json');
+
+// ---------------------------------------------------------------------------
+// Free public endpoints (verified 2026-06-19)
+// ---------------------------------------------------------------------------
+const ARCGIS_BASE =
+  'https://public.gis.lacounty.gov/public/rest/services/LACounty_Cache/LACounty_Parcel/MapServer/0/query';
+
+const ARCGIS_FIELDS = [
+  'APN', 'SitusFullAddress', 'SitusHouseNo', 'SitusStreet', 'SitusZIP',
+  'SitusCity',
+  'YearBuilt1', 'EffectiveYear1',
+  'SQFTmain1', 'Bedrooms1', 'Bathrooms1', 'Units1',
+  'UseDescription', 'DesignType1',
+  'Roll_Year', 'Roll_LandValue', 'Roll_ImpValue',
+  'Roll_LandBaseYear', 'Roll_ImpBaseYear',
+  'Roll_HomeOwnersExemp',
+  'LegalDescription',
+  'CENTER_LAT', 'CENTER_LON',
+].join(',');
+
+// LADBS Socrata datasets (era-bucketed)
+const LADBS_DATASETS = [
+  { id: 'pi9x-tg5x', label: '2020+' },
+  { id: 'dyxf-7hc4', label: '2010-19' },
+  { id: 'e67z-kt2n', label: 'pre-2010' },
+];
+
+// ---------------------------------------------------------------------------
+// HTTP helper — simple https.get that returns a parsed JSON object
+// No retries, 10-second timeout. Never throws to caller (returns null on fail).
+// ---------------------------------------------------------------------------
+function fetchJson(url) {
+  return new Promise(resolve => {
+    const req = https.get(url, { timeout: 10000 }, res => {
+      let body = '';
+      res.on('data', chunk => { body += chunk; });
+      res.on('end', () => {
+        try { resolve(JSON.parse(body)); }
+        catch { resolve(null); }
+      });
+    });
+    req.on('timeout', () => { req.destroy(); resolve(null); });
+    req.on('error', () => resolve(null));
+  });
+}
+
+// ---------------------------------------------------------------------------
+// Address normalisation
+// ---------------------------------------------------------------------------
+const STREET_ABBR = {
+  STREET: 'ST', AVENUE: 'AVE', BOULEVARD: 'BLVD', DRIVE: 'DR',
+  ROAD: 'RD', PLACE: 'PL', TERRACE: 'TER', COURT: 'CT',
+  LANE: 'LN', WAY: 'WAY',
+};
+
+function normalizeAddress(input) {
+  return String(input)
+    .toUpperCase()
+    .replace(/[,]/g, ' ')
+    .replace(/\b(NORTH|SOUTH|EAST|WEST)\b/g, m => m[0])
+    .replace(/\b(STREET|AVENUE|BOULEVARD|DRIVE|ROAD|PLACE|TERRACE|COURT|LANE|WAY)\b/g,
+      m => STREET_ABBR[m] || m)
+    .replace(/\s+/g, ' ')
+    .trim();
+}
+
+/** Extract house number and street tokens from a normalized address string. */
+function parseAddressTokens(norm) {
+  const parts = norm.split(' ');
+  const numIdx = parts.findIndex(p => /^\d+$/.test(p));
+  if (numIdx < 0) return { houseNo: null, street: norm };
+  const houseNo = parts[numIdx];
+  // Street name = tokens after house number, stopping before city/state/ZIP tokens
+  const stopTokens = new Set(['CA', 'LOS', 'ANGELES', 'BEVERLY', 'HILLS', 'SANTA', 'MONICA',
+    'WEST', 'HOLLYWOOD', 'CULVER', 'CITY', 'PASADENA', 'BURBANK', 'GLENDALE',
+    'TARZANA', 'RESEDA', 'WOODLAND', 'ENCINO', 'SHERMAN', 'OAKS', 'VAN', 'NUYS',
+    '91356', '91406', '91335', '90028', '90210', '90036']);
+  const after = parts.slice(numIdx + 1);
+  const stopIdx = after.findIndex(p => stopTokens.has(p) || /^\d{5}$/.test(p));
+  const streetParts = stopIdx >= 0 ? after.slice(0, stopIdx) : after;
+  return { houseNo, street: streetParts.join(' ') };
+}
+
+function escapeLike(s) {
+  return String(s ?? '').replace(/\\/g, '').replace(/'/g, "''").replace(/[%_]/g, '');
+}
+
+// ---------------------------------------------------------------------------
+// Cache helpers
+// ---------------------------------------------------------------------------
+function readCache() {
+  try {
+    const raw = fs.readFileSync(CACHE_PATH, 'utf8');
+    return JSON.parse(raw);
+  } catch {
+    return {};
+  }
+}
+
+function writeCache(cache) {
+  try {
+    fs.mkdirSync(path.dirname(CACHE_PATH), { recursive: true });
+    fs.writeFileSync(CACHE_PATH, JSON.stringify(cache, null, 2), 'utf8');
+  } catch (e) {
+    console.error('[home-history] cache write failed:', e.message);
+  }
+}
+
+// ---------------------------------------------------------------------------
+// ArcGIS parcel lookup
+// ---------------------------------------------------------------------------
+async function lookupParcel(input) {
+  const norm = normalizeAddress(input);
+  const { houseNo, street } = parseAddressTokens(norm);
+
+  if (!houseNo || !street) return null;
+
+  const safeNo     = /^\d{1,8}$/.test(houseNo) ? houseNo : '';
+  const safeStreet = escapeLike(street);
+
+  if (!safeNo || !safeStreet) return null;
+
+  // Try multiple WHERE patterns progressively
+  const wheres = [
+    `SitusHouseNo='${safeNo}' AND SitusStreet='${safeStreet}'`,
+    `SitusFullAddress LIKE '${safeNo} %${safeStreet}%'`,
+    `SitusFullAddress LIKE '${safeNo} W ${safeStreet}%'`,
+    `SitusFullAddress LIKE '${safeNo} N ${safeStreet}%'`,
+  ];
+
+  for (const where of wheres) {
+    const url = `${ARCGIS_BASE}?where=${encodeURIComponent(where)}`
+      + `&outFields=${encodeURIComponent(ARCGIS_FIELDS)}&f=json&resultRecordCount=3`;
+    const data = await fetchJson(url);
+    if (!data) continue;
+    const feats = data.features ?? [];
+    if (feats.length > 0) {
+      return feats[0].attributes;
+    }
+    // Small throttle between ArcGIS requests
+    await new Promise(r => setTimeout(r, 300));
+  }
+  return null;
+}
+
+// ---------------------------------------------------------------------------
+// LADBS permit lookup (by APN, no-dash format)
+// ---------------------------------------------------------------------------
+async function lookupPermits(apn) {
+  if (!apn) return [];
+  const apnClean = String(apn).replace(/-/g, '');
+  const all = [];
+
+  for (const ds of LADBS_DATASETS) {
+    const url = `https://data.lacity.org/resource/${ds.id}.json`
+      + `?$where=${encodeURIComponent(`apn='${apnClean}'`)}&$limit=10&$order=issue_date+DESC`;
+    const data = await fetchJson(url);
+    if (Array.isArray(data)) {
+      all.push(...data.map(p => ({
+        permitNumber: p.permit_nbr,
+        type:         p.permit_type,
+        subType:      p.permit_sub_type,
+        issueDate:    p.issue_date ? p.issue_date.slice(0, 10) : null,
+        status:       p.status_desc,
+        valuation:    p.valuation ? Number(p.valuation) : null,
+        description:  p.work_desc ? p.work_desc.slice(0, 120) : null,
+        dataset:      ds.label,
+      })));
+    }
+    await new Promise(r => setTimeout(r, 300));
+  }
+
+  // Sort most-recent first
+  all.sort((a, b) => (b.issueDate ?? '').localeCompare(a.issueDate ?? ''));
+  return all;
+}
+
+// ---------------------------------------------------------------------------
+// Build normalised result from parcel attributes + permits
+// ---------------------------------------------------------------------------
+function buildResult(address, normalized, parcel, permits) {
+  if (!parcel) {
+    return {
+      address,
+      normalized,
+      builtYear:  null,
+      lastSold:   null,
+      beds:       null,
+      baths:      null,
+      sqft:       null,
+      residents:  [],
+      permits:    [],
+      summary:    'No parcel record found in LA County public GIS layer for this address. '
+                + 'The address may be outside LA County, use a different number format, '
+                + 'or may not be indexed in the current roll.',
+      source:     'unavailable',
+      sourceUrl:  'https://public.gis.lacounty.gov/public/rest/services/LACounty_Cache/LACounty_Parcel/MapServer/0',
+      tier:       'A',
+      retrievedAt: new Date().toISOString(),
+    };
+  }
+
+  const apn         = parcel.APN ?? null;
+  const builtYear   = parcel.YearBuilt1 ? Number(parcel.YearBuilt1) : null;
+  const sqft        = Number(parcel.SQFTmain1) || null;
+  const beds        = Number(parcel.Bedrooms1) || null;
+  const baths       = Number(parcel.Bathrooms1) || null;
+  const landValue   = Number(parcel.Roll_LandValue) || null;
+  const impValue    = Number(parcel.Roll_ImpValue)  || null;
+  const totalAssessed = (landValue && impValue) ? landValue + impValue : null;
+  const landBaseYear  = parcel.Roll_LandBaseYear ?? null;
+  const impBaseYear   = parcel.Roll_ImpBaseYear  ?? null;
+  const useDesc     = parcel.UseDescription ?? null;
+  const situs       = parcel.SitusFullAddress ?? normalized;
+
+  // Last-sold approximation:
+  // Prop 13 resets the "base year" when a property transfers. If both land and
+  // improvement base years are the same recent year, that's the likely last-sale year.
+  // This is an INFERENCE, not a deed transfer record (no free online deed index in LA County).
+  let lastSold = null;
+  if (landBaseYear && landBaseYear === impBaseYear) {
+    const yr = Number(landBaseYear);
+    if (yr > 1980) {
+      lastSold = {
+        date:  `~${yr}`,        // approximate — Prop 13 base year, not exact sale date
+        price: null,            // sale price is NOT in the public assessor layer
+        note:  'Estimated from Prop 13 reassessment base year. '
+             + 'Actual sale date/price requires LA County Registrar-Recorder deed research '
+             + '(no free online index — in-person or CPRA request required).',
+      };
+    }
+  }
+
+  // Residents: NOT available from free sources.
+  // The public parcel layer intentionally omits owner names (Gov Code §6254.21).
+  // City directories are not available as a public API.
+  const residents = [];
+
+  // Permit summary for human reading
+  const recentPermit = permits[0] ?? null;
+  const permitSummary = permits.length > 0
+    ? `${permits.length} building permit(s) on LADBS record; most recent: `
+      + `${recentPermit.type} (${recentPermit.issueDate ?? 'date unknown'}).`
+    : 'No LADBS building permits found in public records.';
+
+  const assessedStr = totalAssessed
+    ? `Total assessed value: $${totalAssessed.toLocaleString()} (${parcel.Roll_Year ?? 'roll year unknown'}).`
+    : '';
+
+  const summary = [
+    `${situs}: ${useDesc ?? 'use type unknown'}.`,
+    builtYear ? `Built ${builtYear}.` : '',
+    sqft      ? `${sqft.toLocaleString()} sq ft.` : '',
+    beds || baths ? `${beds ?? '?'} bed / ${baths ?? '?'} bath.` : '',
+    lastSold  ? `Last transfer approx. ${lastSold.date} (Prop 13 base year reassessment).` : '',
+    assessedStr,
+    permitSummary,
+    apn       ? `APN: ${apn}.` : '',
+    'Owner name, sale price, and resident history are not available from free public sources.',
+  ].filter(Boolean).join(' ');
+
+  return {
+    address,
+    normalized,
+    apn,
+    builtYear,
+    lastSold,
+    beds,
+    baths,
+    sqft,
+    assessedValue: totalAssessed,
+    useDescription: useDesc,
+    residents,           // always empty — see comment above
+    permits,
+    summary,
+    source:     'LA County GIS ArcGIS REST (parcel) + LADBS Socrata (permits)',
+    sourceUrl:  apn
+      ? `https://portal.assessor.lacounty.gov/parceldetail/${apn.replace(/-/g, '')}`
+      : 'https://portal.assessor.lacounty.gov',
+    tier:       'A',
+    retrievedAt: new Date().toISOString(),
+    _raw: {
+      situsFullAddress: situs,
+      landValue,
+      improvementValue: impValue,
+      landBaseYear,
+      improvementBaseYear: impBaseYear,
+      rollYear: parcel.Roll_Year,
+      homeownerExemption: !!parcel.Roll_HomeOwnersExemp,
+      effectiveYear: parcel.EffectiveYear1,
+      lat: Number(parcel.CENTER_LAT) || null,
+      lng: Number(parcel.CENTER_LON) || null,
+    },
+  };
+}
+
+// ---------------------------------------------------------------------------
+// Public API
+// ---------------------------------------------------------------------------
+
+/**
+ * getHomeHistory(address)
+ *
+ * @param {string} address  Free-form address string
+ * @returns {Promise<NormalizedHomeHistory>}
+ *
+ * NormalizedHomeHistory shape:
+ * {
+ *   address:       string,          // original input
+ *   normalized:    string,          // normalised form used for cache key
+ *   apn:           string|null,     // LA County APN (10-digit with dashes)
+ *   builtYear:     number|null,     // year constructed (from assessor roll)
+ *   lastSold:      {date, price, note}|null,  // price is always null (not public)
+ *   beds:          number|null,
+ *   baths:         number|null,
+ *   sqft:          number|null,
+ *   assessedValue: number|null,     // land + improvement (Prop 13 value, NOT market)
+ *   useDescription:string|null,     // e.g. "Single", "Store Combination"
+ *   residents:     [],              // always empty — owner names not public
+ *   permits:       Array<PermitRecord>,
+ *   summary:       string,
+ *   source:        string,          // 'unavailable' if no data found
+ *   sourceUrl:     string,
+ *   tier:          'A'|'B'|'C'|'D',
+ *   retrievedAt:   string,          // ISO-8601
+ *   _raw:          object,          // raw assessor fields (internal use)
+ * }
+ *
+ * Wire into Express:
+ *   const { getHomeHistory } = require('./lib/home-history');
+ *   app.get('/api/home-history', async (req, res) => {
+ *     const result = await getHomeHistory(req.query.address ?? '');
+ *     res.json(result);
+ *   });
+ */
+async function getHomeHistory(address) {
+  if (!address || typeof address !== 'string' || !address.trim()) {
+    return buildResult('', '', null, []);
+  }
+
+  const normalized = normalizeAddress(address.trim());
+
+  // --- Cache check ---
+  const cache = readCache();
+  if (cache[normalized]) {
+    return cache[normalized];
+  }
+
+  // --- Live lookup ---
+  try {
+    const parcel  = await lookupParcel(address);
+    const permits = parcel ? await lookupPermits(parcel.APN) : [];
+    const result  = buildResult(address.trim(), normalized, parcel, permits);
+
+    // Write back to cache
+    cache[normalized] = result;
+    writeCache(cache);
+
+    return result;
+  } catch (err) {
+    console.error('[home-history] lookup error:', err.message);
+    // Return safe unavailable stub — never throw to caller
+    return buildResult(address.trim(), normalized, null, []);
+  }
+}
+
+module.exports = { getHomeHistory };
diff --git a/scripts/seed-home-history.js b/scripts/seed-home-history.js
new file mode 100644
index 0000000..39db473
--- /dev/null
+++ b/scripts/seed-home-history.js
@@ -0,0 +1,45 @@
+/**
+ * seed-home-history.js
+ * One-shot script: warm the home-history cache for all BubbesBlock seed addresses.
+ * Run: node scripts/seed-home-history.js
+ */
+'use strict';
+
+const { getHomeHistory } = require('../lib/home-history');
+
+const ADDRESSES = [
+  // Steve's home
+  '18406 Bessemer Street, Tarzana CA 91406',
+  // Opportunity leads from data/opportunities.json
+  '18722 Yolanda Ave, Tarzana CA 91356',
+  '18450 Bessemer St, Tarzana CA 91406',
+  '5210 Lindley Ave, Tarzana CA 91356',
+  '19010 Etiwanda Ave, Tarzana CA 91356',
+  '18633 Wells Dr, Tarzana CA 91356',
+  '18700 Ventura Blvd, Tarzana CA 91356',
+  '18211 Hart St, Tarzana CA 91335',
+  '18840 Crebs Ave, Tarzana CA 91356',
+  '18509 Clark St, Tarzana CA 91356',
+  '18320 Vanalden Ave, Tarzana CA 91356',
+  '18118 Oso Ave, Tarzana CA 91335',
+  '18555 Reseda Blvd, Tarzana CA 91335',
+];
+
+(async () => {
+  console.log(`Seeding ${ADDRESSES.length} addresses into home-history cache...`);
+  for (const addr of ADDRESSES) {
+    try {
+      const result = await getHomeHistory(addr);
+      const status = result.source === 'unavailable' ? 'UNAVAILABLE' : 'OK';
+      console.log(`[${status}] ${addr}`);
+      if (result.builtYear) {
+        console.log(`       built=${result.builtYear} beds=${result.beds} baths=${result.baths} sqft=${result.sqft} apn=${result.apn}`);
+      }
+    } catch (e) {
+      console.error(`[ERROR] ${addr}: ${e.message}`);
+    }
+    // Throttle between addresses
+    await new Promise(r => setTimeout(r, 600));
+  }
+  console.log('Done. Cache written to data/home-history.json');
+})();

← 552f29c Rebrand: original BubbesBlock logo (row-of-homes + heart) +  ·  back to Bubbesblock  ·  Build out every button with real UX: sessions + claim gate, 7cbd55e →