← 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
A data/home-history.jsonA lib/home-history.jsA scripts/seed-home-history.js
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 →