← back to Commercialrealestate
closed-sales: full LA County coverage — 228 resolved regions, price-banding past the 350 cap, resume ledger, honest ~5yr depth note (6,391 -> 17,023 rows)
cc6c1a95f424f46e028c691069d062d8dde9fa59 · 2026-06-28 10:36:32 -0700 · Steve Abrams
Files touched
M data/redfin-neighborhoods.jsonM data/redfin-regions.jsonM public/sales.htmlM scripts/fetch-closed-sales.jsM scripts/resolve-redfin-neighborhoods.jsM scripts/resolve-redfin-regions.jsM scripts/serve.js
Diff
commit cc6c1a95f424f46e028c691069d062d8dde9fa59
Author: Steve Abrams <steve@designerwallcoverings.com>
Date: Sun Jun 28 10:36:32 2026 -0700
closed-sales: full LA County coverage — 228 resolved regions, price-banding past the 350 cap, resume ledger, honest ~5yr depth note (6,391 -> 17,023 rows)
---
data/redfin-neighborhoods.json | 619 ++++++++++++++++++++++++++++-
data/redfin-regions.json | 670 +++++++++++++++++++++++++++++++-
public/sales.html | 4 +-
scripts/fetch-closed-sales.js | 119 ++++--
scripts/resolve-redfin-neighborhoods.js | 35 +-
scripts/resolve-redfin-regions.js | 40 +-
scripts/serve.js | 4 +-
7 files changed, 1442 insertions(+), 49 deletions(-)
diff --git a/data/redfin-neighborhoods.json b/data/redfin-neighborhoods.json
index c5a5657..eba7099 100644
--- a/data/redfin-neighborhoods.json
+++ b/data/redfin-neighborhoods.json
@@ -1,5 +1,6 @@
{
- "resolved_at": "2026-06-28T14:07:18.399Z",
+ "resolved_at": "2026-06-28T17:25:09.011Z",
+ "count": 93,
"regions": [
{
"query": "Van Nuys",
@@ -128,6 +129,622 @@
"region_id": "14609",
"region_type": 1,
"url": "/neighborhood/14609/CA/Rancho-Palos-Verdes/San-Pedro-Hill"
+ },
+ {
+ "query": "Panorama City",
+ "name": "Panorama City",
+ "sub": "Los Angeles, CA, USA",
+ "region_id": "10731",
+ "region_type": 1,
+ "url": "/neighborhood/10731/CA/Los-Angeles/Panorama-City"
+ },
+ {
+ "query": "Sun Valley",
+ "name": "Sun Valley",
+ "sub": "Los Angeles, CA, USA",
+ "region_id": "10243",
+ "region_type": 1,
+ "url": "/neighborhood/10243/CA/Los-Angeles/Sun-Valley"
+ },
+ {
+ "query": "Granada Hills",
+ "name": "Granada Hills",
+ "sub": "Los Angeles, CA, USA",
+ "region_id": "1126",
+ "region_type": 1,
+ "url": "/neighborhood/1126/CA/Los-Angeles/Granada-Hills"
+ },
+ {
+ "query": "Tarzana",
+ "name": "Tarzana",
+ "sub": "Los Angeles, CA, USA",
+ "region_id": "2692",
+ "region_type": 1,
+ "url": "/neighborhood/2692/CA/Los-Angeles/Tarzana"
+ },
+ {
+ "query": "Winnetka",
+ "name": "Winnetka",
+ "sub": "Los Angeles, CA, USA",
+ "region_id": "3099",
+ "region_type": 1,
+ "url": "/neighborhood/3099/CA/Los-Angeles/Winnetka"
+ },
+ {
+ "query": "West Hills",
+ "name": "West Hills",
+ "sub": "Los Angeles, CA, USA",
+ "region_id": "3006",
+ "region_type": 1,
+ "url": "/neighborhood/3006/CA/Los-Angeles/West-Hills"
+ },
+ {
+ "query": "Lake Balboa",
+ "name": "Lake Balboa",
+ "sub": "Los Angeles, CA, USA",
+ "region_id": "112192",
+ "region_type": 1,
+ "url": "/neighborhood/112192/CA/Los-Angeles/Lake-Balboa"
+ },
+ {
+ "query": "Valley Village",
+ "name": "Valley Village",
+ "sub": "Los Angeles, CA, USA",
+ "region_id": "2852",
+ "region_type": 1,
+ "url": "/neighborhood/2852/CA/Los-Angeles/Valley-Village"
+ },
+ {
+ "query": "Valley Glen",
+ "name": "Valley Glen",
+ "sub": "Los Angeles, CA, USA",
+ "region_id": "2850",
+ "region_type": 1,
+ "url": "/neighborhood/2850/CA/Los-Angeles/Valley-Glen"
+ },
+ {
+ "query": "Toluca Lake",
+ "name": "Greater Toluca Lake",
+ "sub": "Los Angeles, CA, USA",
+ "region_id": "2749",
+ "region_type": 1,
+ "url": "/neighborhood/2749/CA/Los-Angeles/Greater-Toluca-Lake"
+ },
+ {
+ "query": "Arleta",
+ "name": "Arleta",
+ "sub": "Los Angeles, CA, USA",
+ "region_id": "72",
+ "region_type": 1,
+ "url": "/neighborhood/72/CA/Los-Angeles/Arleta"
+ },
+ {
+ "query": "Pacoima",
+ "name": "Pacoima",
+ "sub": "Los Angeles, CA, USA",
+ "region_id": "2079",
+ "region_type": 1,
+ "url": "/neighborhood/2079/CA/Los-Angeles/Pacoima"
+ },
+ {
+ "query": "Mission Hills",
+ "name": "Mission Hills",
+ "sub": "Los Angeles, CA, USA",
+ "region_id": "1734",
+ "region_type": 1,
+ "url": "/neighborhood/1734/CA/Los-Angeles/Mission-Hills"
+ },
+ {
+ "query": "Sylmar",
+ "name": "Sylmar",
+ "sub": "Los Angeles, CA, USA",
+ "region_id": "2675",
+ "region_type": 1,
+ "url": "/neighborhood/2675/CA/Los-Angeles/Sylmar"
+ },
+ {
+ "query": "Tujunga",
+ "name": "Sunland-Tujunga",
+ "sub": "Los Angeles, CA, USA",
+ "region_id": "2642",
+ "region_type": 1,
+ "url": "/neighborhood/2642/CA/Los-Angeles/Sunland-Tujunga"
+ },
+ {
+ "query": "Sunland",
+ "name": "Sunland",
+ "sub": "Los Angeles, CA, USA",
+ "region_id": "9719",
+ "region_type": 1,
+ "url": "/neighborhood/9719/CA/Los-Angeles/Sunland"
+ },
+ {
+ "query": "Porter Ranch",
+ "name": "Porter Ranch",
+ "sub": "Los Angeles, CA, USA",
+ "region_id": "2179",
+ "region_type": 1,
+ "url": "/neighborhood/2179/CA/Los-Angeles/Porter-Ranch"
+ },
+ {
+ "query": "Chatsworth",
+ "name": "Chatsworth",
+ "sub": "Los Angeles, CA, USA",
+ "region_id": "460",
+ "region_type": 1,
+ "url": "/neighborhood/460/CA/Los-Angeles/Chatsworth"
+ },
+ {
+ "query": "North Hills",
+ "name": "North Hills",
+ "sub": "Los Angeles, CA, USA",
+ "region_id": "351587",
+ "region_type": 1,
+ "url": "/neighborhood/351587/CA/Los-Angeles/North-Hills"
+ },
+ {
+ "query": "Sepulveda",
+ "name": "Sepulveda",
+ "sub": "Glendale, CA, USA",
+ "region_id": "14610",
+ "region_type": 1,
+ "url": "/neighborhood/14610/CA/Glendale/Sepulveda"
+ },
+ {
+ "query": "Shadow Hills",
+ "name": "Shadow Hills",
+ "sub": "Los Angeles, CA, USA",
+ "region_id": "11702",
+ "region_type": 1,
+ "url": "/neighborhood/11702/CA/Los-Angeles/Shadow-Hills"
+ },
+ {
+ "query": "Lake View Terrace",
+ "name": "Lake View Terrace",
+ "sub": "Los Angeles, CA, USA",
+ "region_id": "10632",
+ "region_type": 1,
+ "url": "/neighborhood/10632/CA/Los-Angeles/Lake-View-Terrace"
+ },
+ {
+ "query": "Cahuenga Pass",
+ "name": "Cahuenga Pass",
+ "sub": "Los Angeles, CA, USA",
+ "region_id": "59009",
+ "region_type": 1,
+ "url": "/neighborhood/59009/CA/Los-Angeles/Cahuenga-Pass"
+ },
+ {
+ "query": "Brentwood",
+ "name": "Brentwood",
+ "sub": "Los Angeles, CA, USA",
+ "region_id": "258",
+ "region_type": 1,
+ "url": "/neighborhood/258/CA/Los-Angeles/Brentwood"
+ },
+ {
+ "query": "Westwood",
+ "name": "Westwood",
+ "sub": "Los Angeles, CA, USA",
+ "region_id": "3055",
+ "region_type": 1,
+ "url": "/neighborhood/3055/CA/Los-Angeles/Westwood"
+ },
+ {
+ "query": "West Los Angeles",
+ "name": "Los Nietos Junction",
+ "sub": "West Whittier-Los Nietos, CA, USA",
+ "region_id": "14587",
+ "region_type": 1,
+ "url": "/neighborhood/14587/CA/West-Whittier-Los-Nietos/Los-Nietos-Junction"
+ },
+ {
+ "query": "Sawtelle",
+ "name": "Sawtelle",
+ "sub": "Los Angeles, CA, USA",
+ "region_id": "9168",
+ "region_type": 1,
+ "url": "/neighborhood/9168/CA/Los-Angeles/Sawtelle"
+ },
+ {
+ "query": "Playa Del Rey",
+ "name": "Playa del Rey",
+ "sub": "Los Angeles, CA, USA",
+ "region_id": "3844",
+ "region_type": 1,
+ "url": "/neighborhood/3844/CA/Los-Angeles/Playa-del-Rey"
+ },
+ {
+ "query": "Playa Vista",
+ "name": "Playa Vista",
+ "sub": "Los Angeles, CA, USA",
+ "region_id": "496706",
+ "region_type": 1,
+ "url": "/neighborhood/496706/CA/Los-Angeles/Playa-Vista"
+ },
+ {
+ "query": "Palms",
+ "name": "Palms",
+ "sub": "Los Angeles, CA, USA",
+ "region_id": "2083",
+ "region_type": 1,
+ "url": "/neighborhood/2083/CA/Los-Angeles/Palms"
+ },
+ {
+ "query": "Cheviot Hills",
+ "name": "Cheviot Hills",
+ "sub": "Los Angeles, CA, USA",
+ "region_id": "10493",
+ "region_type": 1,
+ "url": "/neighborhood/10493/CA/Los-Angeles/Cheviot-Hills"
+ },
+ {
+ "query": "Rancho Park",
+ "name": "Rancho Park",
+ "sub": "Los Angeles, CA, USA",
+ "region_id": "9744",
+ "region_type": 1,
+ "url": "/neighborhood/9744/CA/Los-Angeles/Rancho-Park"
+ },
+ {
+ "query": "Century City",
+ "name": "Century City",
+ "sub": "Los Angeles, CA, USA",
+ "region_id": "446",
+ "region_type": 1,
+ "url": "/neighborhood/446/CA/Los-Angeles/Century-City"
+ },
+ {
+ "query": "Beverly Crest",
+ "name": "Beverly Crest",
+ "sub": "Los Angeles, CA, USA",
+ "region_id": "10825",
+ "region_type": 1,
+ "url": "/neighborhood/10825/CA/Los-Angeles/Beverly-Crest"
+ },
+ {
+ "query": "Bel Air",
+ "name": "Bel Air",
+ "sub": "Los Angeles, CA, USA",
+ "region_id": "760389",
+ "region_type": 1,
+ "url": "/neighborhood/760389/CA/Los-Angeles/Bel-Air"
+ },
+ {
+ "query": "Bel-Air",
+ "name": "Bel Air-Beverly Crest",
+ "sub": "Los Angeles, CA, USA",
+ "region_id": "169",
+ "region_type": 1,
+ "url": "/neighborhood/169/CA/Los-Angeles/Bel-Air-Beverly-Crest"
+ },
+ {
+ "query": "Hollywood",
+ "name": "Hollywood Hills",
+ "sub": "Los Angeles, CA, USA",
+ "region_id": "547223",
+ "region_type": 1,
+ "url": "/neighborhood/547223/CA/Los-Angeles/Hollywood-Hills"
+ },
+ {
+ "query": "East Hollywood",
+ "name": "East Hollywood",
+ "sub": "Los Angeles, CA, USA",
+ "region_id": "811",
+ "region_type": 1,
+ "url": "/neighborhood/811/CA/Los-Angeles/East-Hollywood"
+ },
+ {
+ "query": "West Hollywood",
+ "name": "North Hollywood West",
+ "sub": "Los Angeles, CA, USA",
+ "region_id": "547227",
+ "region_type": 1,
+ "url": "/neighborhood/547227/CA/Los-Angeles/North-Hollywood-West"
+ },
+ {
+ "query": "Hancock Park",
+ "name": "Hancock Park",
+ "sub": "Los Angeles, CA, USA",
+ "region_id": "110322",
+ "region_type": 1,
+ "url": "/neighborhood/110322/CA/Los-Angeles/Hancock-Park"
+ },
+ {
+ "query": "Larchmont",
+ "name": "Larchmont",
+ "sub": "Los Angeles, CA, USA",
+ "region_id": "113723",
+ "region_type": 1,
+ "url": "/neighborhood/113723/CA/Los-Angeles/Larchmont"
+ },
+ {
+ "query": "Mid-City",
+ "name": "Mid-City",
+ "sub": "Santa Monica, CA, USA",
+ "region_id": "1692",
+ "region_type": 1,
+ "url": "/neighborhood/1692/CA/Santa-Monica/Mid-City"
+ },
+ {
+ "query": "Mid City",
+ "name": "Mid City",
+ "sub": "Los Angeles, CA, USA",
+ "region_id": "9917",
+ "region_type": 1,
+ "url": "/neighborhood/9917/CA/Los-Angeles/Mid-City"
+ },
+ {
+ "query": "Mid-Wilshire",
+ "name": "Mid-Wilshire",
+ "sub": "Los Angeles, CA, USA",
+ "region_id": "547225",
+ "region_type": 1,
+ "url": "/neighborhood/547225/CA/Los-Angeles/Mid-Wilshire"
+ },
+ {
+ "query": "Pico-Union",
+ "name": "Pico Union",
+ "sub": "Los Angeles, CA, USA",
+ "region_id": "2140",
+ "region_type": 1,
+ "url": "/neighborhood/2140/CA/Los-Angeles/Pico-Union"
+ },
+ {
+ "query": "Westlake",
+ "name": "Westlake",
+ "sub": "Los Angeles, CA, USA",
+ "region_id": "10094",
+ "region_type": 1,
+ "url": "/neighborhood/10094/CA/Los-Angeles/Westlake"
+ },
+ {
+ "query": "Silver Lake",
+ "name": "Silver Lake",
+ "sub": "Los Angeles, CA, USA",
+ "region_id": "7511",
+ "region_type": 1,
+ "url": "/neighborhood/7511/CA/Los-Angeles/Silver-Lake"
+ },
+ {
+ "query": "Silverlake",
+ "name": "Ivanhoe / Silverlake",
+ "sub": "Los Angeles, CA, USA",
+ "region_id": "1369",
+ "region_type": 1,
+ "url": "/neighborhood/1369/CA/Los-Angeles/Ivanhoe-Silverlake"
+ },
+ {
+ "query": "Los Feliz",
+ "name": "Los Feliz",
+ "sub": "Los Angeles, CA, USA",
+ "region_id": "9961",
+ "region_type": 1,
+ "url": "/neighborhood/9961/CA/Los-Angeles/Los-Feliz"
+ },
+ {
+ "query": "Atwater Village",
+ "name": "Atwater Village",
+ "sub": "Los Angeles, CA, USA",
+ "region_id": "104",
+ "region_type": 1,
+ "url": "/neighborhood/104/CA/Los-Angeles/Atwater-Village"
+ },
+ {
+ "query": "Eagle Rock",
+ "name": "Eagle Rock",
+ "sub": "Los Angeles, CA, USA",
+ "region_id": "786",
+ "region_type": 1,
+ "url": "/neighborhood/786/CA/Los-Angeles/Eagle-Rock"
+ },
+ {
+ "query": "Mount Washington",
+ "name": "Mount Washington",
+ "sub": "Los Angeles, CA, USA",
+ "region_id": "1790",
+ "region_type": 1,
+ "url": "/neighborhood/1790/CA/Los-Angeles/Mount-Washington"
+ },
+ {
+ "query": "Glassell Park",
+ "name": "Glassell Park",
+ "sub": "Los Angeles, CA, USA",
+ "region_id": "1085",
+ "region_type": 1,
+ "url": "/neighborhood/1085/CA/Los-Angeles/Glassell-Park"
+ },
+ {
+ "query": "Cypress Park",
+ "name": "Cypress Park",
+ "sub": "Los Angeles, CA, USA",
+ "region_id": "661",
+ "region_type": 1,
+ "url": "/neighborhood/661/CA/Los-Angeles/Cypress-Park"
+ },
+ {
+ "query": "Lincoln Heights",
+ "name": "Lincoln Heights",
+ "sub": "Los Angeles, CA, USA",
+ "region_id": "1531",
+ "region_type": 1,
+ "url": "/neighborhood/1531/CA/Los-Angeles/Lincoln-Heights"
+ },
+ {
+ "query": "El Sereno",
+ "name": "El Sereno",
+ "sub": "Los Angeles, CA, USA",
+ "region_id": "888",
+ "region_type": 1,
+ "url": "/neighborhood/888/CA/Los-Angeles/El-Sereno"
+ },
+ {
+ "query": "Boyle Heights",
+ "name": "Boyle Heights",
+ "sub": "Los Angeles, CA, USA",
+ "region_id": "247",
+ "region_type": 1,
+ "url": "/neighborhood/247/CA/Los-Angeles/Boyle-Heights"
+ },
+ {
+ "query": "Downtown Los Angeles",
+ "name": "Downtown Los Angeles",
+ "sub": "Los Angeles, CA, USA",
+ "region_id": "290722",
+ "region_type": 1,
+ "url": "/neighborhood/290722/CA/Los-Angeles/Downtown-Los-Angeles"
+ },
+ {
+ "query": "Arts District",
+ "name": "Arts District",
+ "sub": "Los Angeles, CA, USA",
+ "region_id": "91",
+ "region_type": 1,
+ "url": "/neighborhood/91/CA/Los-Angeles/Arts-District"
+ },
+ {
+ "query": "Chinatown",
+ "name": "Chinatown",
+ "sub": "Los Angeles, CA, USA",
+ "region_id": "483",
+ "region_type": 1,
+ "url": "/neighborhood/483/CA/Los-Angeles/Chinatown"
+ },
+ {
+ "query": "Historic Filipinotown",
+ "name": "Historic Filipinotown",
+ "sub": "Los Angeles, CA, USA",
+ "region_id": "62693",
+ "region_type": 1,
+ "url": "/neighborhood/62693/CA/Los-Angeles/Historic-Filipinotown"
+ },
+ {
+ "query": "South Los Angeles",
+ "name": "Los Altos South",
+ "sub": "Long Beach, CA, USA",
+ "region_id": "1565",
+ "region_type": 1,
+ "url": "/neighborhood/1565/CA/Long-Beach/Los-Altos-South"
+ },
+ {
+ "query": "South La",
+ "name": "South LA",
+ "sub": "Los Angeles, CA, USA",
+ "region_id": "92255",
+ "region_type": 1,
+ "url": "/neighborhood/92255/CA/Los-Angeles/South-LA"
+ },
+ {
+ "query": "Jefferson Park",
+ "name": "Jefferson Park",
+ "sub": "Los Angeles, CA, USA",
+ "region_id": "3958",
+ "region_type": 1,
+ "url": "/neighborhood/3958/CA/Los-Angeles/Jefferson-Park"
+ },
+ {
+ "query": "West Adams",
+ "name": "West Adams",
+ "sub": "Los Angeles, CA, USA",
+ "region_id": "2967",
+ "region_type": 1,
+ "url": "/neighborhood/2967/CA/Los-Angeles/West-Adams"
+ },
+ {
+ "query": "Leimert Park",
+ "name": "Leimert Park",
+ "sub": "Los Angeles, CA, USA",
+ "region_id": "10210",
+ "region_type": 1,
+ "url": "/neighborhood/10210/CA/Los-Angeles/Leimert-Park"
+ },
+ {
+ "query": "Baldwin Hills",
+ "name": "Baldwin Hills",
+ "sub": "Los Angeles, CA, USA",
+ "region_id": "10309",
+ "region_type": 1,
+ "url": "/neighborhood/10309/CA/Los-Angeles/Baldwin-Hills"
+ },
+ {
+ "query": "Crenshaw",
+ "name": "Crenshaw",
+ "sub": "Los Angeles, CA, USA",
+ "region_id": "633",
+ "region_type": 1,
+ "url": "/neighborhood/633/CA/Los-Angeles/Crenshaw"
+ },
+ {
+ "query": "View Park",
+ "name": "View Park",
+ "sub": "View Park-Windsor Hills, CA, USA",
+ "region_id": "9726",
+ "region_type": 1,
+ "url": "/neighborhood/9726/CA/View-Park-Windsor-Hills/View-Park"
+ },
+ {
+ "query": "Windsor Hills",
+ "name": "Windsor Hills",
+ "sub": "View Park-Windsor Hills, CA, USA",
+ "region_id": "9865",
+ "region_type": 1,
+ "url": "/neighborhood/9865/CA/View-Park-Windsor-Hills/Windsor-Hills"
+ },
+ {
+ "query": "Watts",
+ "name": "Watts",
+ "sub": "Los Angeles, CA, USA",
+ "region_id": "2946",
+ "region_type": 1,
+ "url": "/neighborhood/2946/CA/Los-Angeles/Watts"
+ },
+ {
+ "query": "Wilmington",
+ "name": "Wilmington",
+ "sub": "Los Angeles, CA, USA",
+ "region_id": "113062",
+ "region_type": 1,
+ "url": "/neighborhood/113062/CA/Los-Angeles/Wilmington"
+ },
+ {
+ "query": "Harbor City",
+ "name": "Harbor City",
+ "sub": "Los Angeles, CA, USA",
+ "region_id": "1192",
+ "region_type": 1,
+ "url": "/neighborhood/1192/CA/Los-Angeles/Harbor-City"
+ },
+ {
+ "query": "Harbor Gateway",
+ "name": "Harbor Gateway North",
+ "sub": "Los Angeles, CA, USA",
+ "region_id": "119076",
+ "region_type": 1,
+ "url": "/neighborhood/119076/CA/Los-Angeles/Harbor-Gateway-North"
+ },
+ {
+ "query": "South Park",
+ "name": "Park South",
+ "sub": "Calabasas, CA, USA",
+ "region_id": "118902",
+ "region_type": 1,
+ "url": "/neighborhood/118902/CA/Calabasas/Park-South"
+ },
+ {
+ "query": "University Park",
+ "name": "University Park",
+ "sub": "Los Angeles, CA, USA",
+ "region_id": "116045",
+ "region_type": 1,
+ "url": "/neighborhood/116045/CA/Los-Angeles/University-Park"
+ },
+ {
+ "query": "Exposition Park",
+ "name": "Exposition Park",
+ "sub": "Los Angeles, CA, USA",
+ "region_id": "61153",
+ "region_type": 1,
+ "url": "/neighborhood/61153/CA/Los-Angeles/Exposition-Park"
}
]
}
\ No newline at end of file
diff --git a/data/redfin-regions.json b/data/redfin-regions.json
index c0a9e9f..e2ce7d3 100644
--- a/data/redfin-regions.json
+++ b/data/redfin-regions.json
@@ -1,6 +1,6 @@
{
- "resolved_at": "2026-06-28T14:06:30.766Z",
- "count": 25,
+ "resolved_at": "2026-06-28T17:24:12.760Z",
+ "count": 136,
"regions": [
{
"query": "Van Nuys",
@@ -215,6 +215,672 @@
"name": "Santa Clarita",
"region_id": "17676",
"url": "/city/17676/CA/Santa-Clarita"
+ },
+ {
+ "query": "Agoura Hills",
+ "name": "Agoura Hills",
+ "region_id": "77",
+ "url": "/city/77/CA/Agoura-Hills"
+ },
+ {
+ "query": "Alhambra",
+ "name": "Alhambra",
+ "region_id": "208",
+ "url": "/city/208/CA/Alhambra"
+ },
+ {
+ "query": "Arcadia",
+ "name": "Arcadia",
+ "region_id": "645",
+ "url": "/city/645/CA/Arcadia"
+ },
+ {
+ "query": "Artesia",
+ "name": "Artesia",
+ "region_id": "761",
+ "url": "/city/761/CA/Artesia"
+ },
+ {
+ "query": "Avalon",
+ "name": "Avalon",
+ "region_id": "878",
+ "url": "/city/878/CA/Avalon"
+ },
+ {
+ "query": "Azusa",
+ "name": "Azusa",
+ "region_id": "921",
+ "url": "/city/921/CA/Azusa"
+ },
+ {
+ "query": "Baldwin Park",
+ "name": "Baldwin Park",
+ "region_id": "998",
+ "url": "/city/998/CA/Baldwin-Park"
+ },
+ {
+ "query": "Bell",
+ "name": "Bell",
+ "region_id": "1285",
+ "url": "/city/1285/CA/Bell"
+ },
+ {
+ "query": "Bell Gardens",
+ "name": "Bell Gardens",
+ "region_id": "1331",
+ "url": "/city/1331/CA/Bell-Gardens"
+ },
+ {
+ "query": "Bellflower",
+ "name": "Bellflower",
+ "region_id": "1328",
+ "url": "/city/1328/CA/Bellflower"
+ },
+ {
+ "query": "Bradbury",
+ "name": "Bradbury",
+ "region_id": "2048",
+ "url": "/city/2048/CA/Bradbury"
+ },
+ {
+ "query": "Calabasas",
+ "name": "Calabasas",
+ "region_id": "2471",
+ "url": "/city/2471/CA/Calabasas"
+ },
+ {
+ "query": "Carson",
+ "name": "Carson",
+ "region_id": "2990",
+ "url": "/city/2990/CA/Carson"
+ },
+ {
+ "query": "Cerritos",
+ "name": "Cerritos",
+ "region_id": "3268",
+ "url": "/city/3268/CA/Cerritos"
+ },
+ {
+ "query": "Claremont",
+ "name": "Claremont",
+ "region_id": "3578",
+ "url": "/city/3578/CA/Claremont"
+ },
+ {
+ "query": "Commerce",
+ "name": "Commerce",
+ "region_id": "3870",
+ "url": "/city/3870/CA/Commerce"
+ },
+ {
+ "query": "Compton",
+ "name": "Compton",
+ "region_id": "3890",
+ "url": "/city/3890/CA/Compton"
+ },
+ {
+ "query": "Covina",
+ "name": "Covina",
+ "region_id": "4367",
+ "url": "/city/4367/CA/Covina"
+ },
+ {
+ "query": "Cudahy",
+ "name": "Cudahy",
+ "region_id": "4539",
+ "url": "/city/4539/CA/Cudahy"
+ },
+ {
+ "query": "Diamond Bar",
+ "name": "Diamond Bar",
+ "region_id": "4957",
+ "url": "/city/4957/CA/Diamond-Bar"
+ },
+ {
+ "query": "Duarte",
+ "name": "Duarte",
+ "region_id": "5152",
+ "url": "/city/5152/CA/Duarte"
+ },
+ {
+ "query": "El Segundo",
+ "name": "El Segundo",
+ "region_id": "5771",
+ "url": "/city/5771/CA/El-Segundo"
+ },
+ {
+ "query": "Glendora",
+ "name": "Glendora",
+ "region_id": "7650",
+ "url": "/city/7650/CA/Glendora"
+ },
+ {
+ "query": "Hawaiian Gardens",
+ "name": "Hawaiian Gardens",
+ "region_id": "8292",
+ "url": "/city/8292/CA/Hawaiian-Gardens"
+ },
+ {
+ "query": "Hermosa Beach",
+ "name": "Hermosa Beach",
+ "region_id": "8536",
+ "url": "/city/8536/CA/Hermosa-Beach"
+ },
+ {
+ "query": "Hidden Hills",
+ "name": "Hidden Hills",
+ "region_id": "8583",
+ "url": "/city/8583/CA/Hidden-Hills"
+ },
+ {
+ "query": "Huntington Park",
+ "name": "Huntington Park",
+ "region_id": "9186",
+ "url": "/city/9186/CA/Huntington-Park"
+ },
+ {
+ "query": "Industry",
+ "name": "Industry",
+ "region_id": "9288",
+ "url": "/city/9288/CA/Industry"
+ },
+ {
+ "query": "Irwindale",
+ "name": "Irwindale",
+ "region_id": "9373",
+ "url": "/city/9373/CA/Irwindale"
+ },
+ {
+ "query": "La Canada Flintridge",
+ "name": "La Cañada Flintridge",
+ "region_id": "9895",
+ "url": "/city/9895/CA/La-Caada-Flintridge"
+ },
+ {
+ "query": "La Habra Heights",
+ "name": "La Habra Heights",
+ "region_id": "9982",
+ "url": "/city/9982/CA/La-Habra-Heights"
+ },
+ {
+ "query": "La Mirada",
+ "name": "La Mirada",
+ "region_id": "10208",
+ "url": "/city/10208/CA/La-Mirada"
+ },
+ {
+ "query": "La Puente",
+ "name": "La Puente",
+ "region_id": "10290",
+ "url": "/city/10290/CA/La-Puente"
+ },
+ {
+ "query": "La Verne",
+ "name": "La Verne",
+ "region_id": "10417",
+ "url": "/city/10417/CA/La-Verne"
+ },
+ {
+ "query": "Lakewood",
+ "name": "Lakewood",
+ "region_id": "10167",
+ "url": "/city/10167/CA/Lakewood"
+ },
+ {
+ "query": "Lawndale",
+ "name": "Lawndale",
+ "region_id": "10433",
+ "url": "/city/10433/CA/Lawndale"
+ },
+ {
+ "query": "Lomita",
+ "name": "Lomita",
+ "region_id": "10810",
+ "url": "/city/10810/CA/Lomita"
+ },
+ {
+ "query": "Lynwood",
+ "name": "Lynwood",
+ "region_id": "11368",
+ "url": "/city/11368/CA/Lynwood"
+ },
+ {
+ "query": "Manhattan Beach",
+ "name": "Manhattan Beach",
+ "region_id": "11576",
+ "url": "/city/11576/CA/Manhattan-Beach"
+ },
+ {
+ "query": "Maywood",
+ "name": "Maywood",
+ "region_id": "11864",
+ "url": "/city/11864/CA/Maywood"
+ },
+ {
+ "query": "Monrovia",
+ "name": "Monrovia",
+ "region_id": "12449",
+ "url": "/city/12449/CA/Monrovia"
+ },
+ {
+ "query": "Montebello",
+ "name": "Montebello",
+ "region_id": "12502",
+ "url": "/city/12502/CA/Montebello"
+ },
+ {
+ "query": "Monterey Park",
+ "name": "Monterey Park",
+ "region_id": "12524",
+ "url": "/city/12524/CA/Monterey-Park"
+ },
+ {
+ "query": "Norwalk",
+ "name": "Norwalk",
+ "region_id": "13533",
+ "url": "/city/13533/CA/Norwalk"
+ },
+ {
+ "query": "Palos Verdes Estates",
+ "name": "Palos Verdes Estates",
+ "region_id": "14343",
+ "url": "/city/14343/CA/Palos-Verdes-Estates"
+ },
+ {
+ "query": "Paramount",
+ "name": "Paramount",
+ "region_id": "14399",
+ "url": "/city/14399/CA/Paramount"
+ },
+ {
+ "query": "Pico Rivera",
+ "name": "Pico Rivera",
+ "region_id": "14737",
+ "url": "/city/14737/CA/Pico-Rivera"
+ },
+ {
+ "query": "Rancho Palos Verdes",
+ "name": "Rancho Palos Verdes",
+ "region_id": "15404",
+ "url": "/city/15404/CA/Rancho-Palos-Verdes"
+ },
+ {
+ "query": "Redondo Beach",
+ "name": "Redondo Beach",
+ "region_id": "15507",
+ "url": "/city/15507/CA/Redondo-Beach"
+ },
+ {
+ "query": "Rolling Hills",
+ "name": "Rolling Hills",
+ "region_id": "16076",
+ "url": "/city/16076/CA/Rolling-Hills"
+ },
+ {
+ "query": "Rolling Hills Estates",
+ "name": "Rolling Hills Estates",
+ "region_id": "16085",
+ "url": "/city/16085/CA/Rolling-Hills-Estates"
+ },
+ {
+ "query": "Rosemead",
+ "name": "Rosemead",
+ "region_id": "16137",
+ "url": "/city/16137/CA/Rosemead"
+ },
+ {
+ "query": "San Dimas",
+ "name": "San Dimas",
+ "region_id": "16923",
+ "url": "/city/16923/CA/San-Dimas"
+ },
+ {
+ "query": "San Fernando",
+ "name": "San Fernando",
+ "region_id": "16938",
+ "url": "/city/16938/CA/San-Fernando"
+ },
+ {
+ "query": "San Gabriel",
+ "name": "San Gabriel",
+ "region_id": "17164",
+ "url": "/city/17164/CA/San-Gabriel"
+ },
+ {
+ "query": "San Marino",
+ "name": "San Marino",
+ "region_id": "17481",
+ "url": "/city/17481/CA/San-Marino"
+ },
+ {
+ "query": "Santa Fe Springs",
+ "name": "Santa Fe Springs",
+ "region_id": "17691",
+ "url": "/city/17691/CA/Santa-Fe-Springs"
+ },
+ {
+ "query": "Sierra Madre",
+ "name": "Sierra Madre",
+ "region_id": "18335",
+ "url": "/city/18335/CA/Sierra-Madre"
+ },
+ {
+ "query": "Signal Hill",
+ "name": "Signal Hill",
+ "region_id": "18347",
+ "url": "/city/18347/CA/Signal-Hill"
+ },
+ {
+ "query": "South El Monte",
+ "name": "South El Monte",
+ "region_id": "18603",
+ "url": "/city/18603/CA/South-El-Monte"
+ },
+ {
+ "query": "South Gate",
+ "name": "South Gate",
+ "region_id": "18624",
+ "url": "/city/18624/CA/South-Gate"
+ },
+ {
+ "query": "South Pasadena",
+ "name": "South Pasadena",
+ "region_id": "18661",
+ "url": "/city/18661/CA/South-Pasadena"
+ },
+ {
+ "query": "Temple City",
+ "name": "Temple City",
+ "region_id": "19707",
+ "url": "/city/19707/CA/Temple-City"
+ },
+ {
+ "query": "Vernon",
+ "name": "Vernon",
+ "region_id": "20486",
+ "url": "/city/20486/CA/Vernon"
+ },
+ {
+ "query": "Walnut",
+ "name": "Walnut",
+ "region_id": "20628",
+ "url": "/city/20628/CA/Walnut"
+ },
+ {
+ "query": "West Covina",
+ "name": "West Covina",
+ "region_id": "20753",
+ "url": "/city/20753/CA/West-Covina"
+ },
+ {
+ "query": "Westlake Village",
+ "name": "Westlake Village",
+ "region_id": "20777",
+ "url": "/city/20777/CA/Westlake-Village"
+ },
+ {
+ "query": "Marina Del Rey",
+ "name": "Marina del Rey",
+ "region_id": "24172",
+ "url": "/city/24172/CA/Marina-del-Rey"
+ },
+ {
+ "query": "Topanga",
+ "name": "Topanga",
+ "region_id": "35311",
+ "url": "/city/35311/CA/Topanga"
+ },
+ {
+ "query": "East Los Angeles",
+ "name": "East Los Angeles",
+ "region_id": "22411",
+ "url": "/city/22411/CA/East-Los-Angeles"
+ },
+ {
+ "query": "East La",
+ "name": "East La Mirada",
+ "region_id": "22409",
+ "url": "/city/22409/CA/East-La-Mirada"
+ },
+ {
+ "query": "Ladera Heights",
+ "name": "Ladera Heights",
+ "region_id": "23650",
+ "url": "/city/23650/CA/Ladera-Heights"
+ },
+ {
+ "query": "Lennox",
+ "name": "Lennox",
+ "region_id": "23841",
+ "url": "/city/23841/CA/Lennox"
+ },
+ {
+ "query": "Del Aire",
+ "name": "Del Aire",
+ "region_id": "22254",
+ "url": "/city/22254/CA/Del-Aire"
+ },
+ {
+ "query": "Westmont",
+ "name": "Westmont",
+ "region_id": "26746",
+ "url": "/city/26746/CA/Westmont"
+ },
+ {
+ "query": "Florence-Graham",
+ "name": "Florence-Graham",
+ "region_id": "22672",
+ "url": "/city/22672/CA/Florence-Graham"
+ },
+ {
+ "query": "Willowbrook",
+ "name": "Willowbrook",
+ "region_id": "26788",
+ "url": "/city/26788/CA/Willowbrook"
+ },
+ {
+ "query": "Walnut Park",
+ "name": "Walnut Park",
+ "region_id": "26696",
+ "url": "/city/26696/CA/Walnut-Park"
+ },
+ {
+ "query": "West Whittier",
+ "name": "West Whittier-Los Nietos",
+ "region_id": "26760",
+ "url": "/city/26760/CA/West-Whittier-Los-Nietos"
+ },
+ {
+ "query": "South Whittier",
+ "name": "South Whittier",
+ "region_id": "26107",
+ "url": "/city/26107/CA/South-Whittier"
+ },
+ {
+ "query": "Hacienda Heights",
+ "name": "Hacienda Heights",
+ "region_id": "23174",
+ "url": "/city/23174/CA/Hacienda-Heights"
+ },
+ {
+ "query": "Rowland Heights",
+ "name": "Rowland Heights",
+ "region_id": "25415",
+ "url": "/city/25415/CA/Rowland-Heights"
+ },
+ {
+ "query": "Avocado Heights",
+ "name": "Avocado Heights",
+ "region_id": "21303",
+ "url": "/city/21303/CA/Avocado-Heights"
+ },
+ {
+ "query": "Valinda",
+ "name": "Valinda",
+ "region_id": "26623",
+ "url": "/city/26623/CA/Valinda"
+ },
+ {
+ "query": "Charter Oak",
+ "name": "Charter Oak",
+ "region_id": "21898",
+ "url": "/city/21898/CA/Charter-Oak"
+ },
+ {
+ "query": "Citrus",
+ "name": "Citrus Heights",
+ "region_id": "3539",
+ "url": "/city/3539/CA/Citrus-Heights"
+ },
+ {
+ "query": "Vincent",
+ "name": "Vincent",
+ "region_id": "31340",
+ "url": "/city/31340/CA/Vincent"
+ },
+ {
+ "query": "South San Gabriel",
+ "name": "South San Gabriel",
+ "region_id": "26088",
+ "url": "/city/26088/CA/South-San-Gabriel"
+ },
+ {
+ "query": "East Pasadena",
+ "name": "East Pasadena",
+ "region_id": "22426",
+ "url": "/city/22426/CA/East-Pasadena"
+ },
+ {
+ "query": "East San Gabriel",
+ "name": "East San Gabriel",
+ "region_id": "22439",
+ "url": "/city/22439/CA/East-San-Gabriel"
+ },
+ {
+ "query": "Kinneloa Mesa",
+ "name": "Kinneloa Mesa",
+ "region_id": "28298",
+ "url": "/city/28298/CA/Kinneloa-Mesa"
+ },
+ {
+ "query": "Mayflower Village",
+ "name": "Mayflower Village",
+ "region_id": "24216",
+ "url": "/city/24216/CA/Mayflower-Village"
+ },
+ {
+ "query": "North El Monte",
+ "name": "North El Monte",
+ "region_id": "24615",
+ "url": "/city/24615/CA/North-El-Monte"
+ },
+ {
+ "query": "South Monrovia Island",
+ "name": "South Monrovia Island",
+ "region_id": "35036",
+ "url": "/city/35036/CA/South-Monrovia-Island"
+ },
+ {
+ "query": "Stevenson Ranch",
+ "name": "Stevenson Ranch",
+ "region_id": "35080",
+ "url": "/city/35080/CA/Stevenson-Ranch"
+ },
+ {
+ "query": "Castaic",
+ "name": "Castaic",
+ "region_id": "35836",
+ "url": "/city/35836/CA/Castaic"
+ },
+ {
+ "query": "Val Verde",
+ "name": "Val Verde",
+ "region_id": "26642",
+ "url": "/city/26642/CA/Val-Verde"
+ },
+ {
+ "query": "Agua Dulce",
+ "name": "Agua Dulce",
+ "region_id": "28258",
+ "url": "/city/28258/CA/Agua-Dulce"
+ },
+ {
+ "query": "Acton",
+ "name": "Acton",
+ "region_id": "21123",
+ "url": "/city/21123/CA/Acton"
+ },
+ {
+ "query": "Littlerock",
+ "name": "Littlerock",
+ "region_id": "23908",
+ "url": "/city/23908/CA/Littlerock"
+ },
+ {
+ "query": "Quartz Hill",
+ "name": "Quartz Hill",
+ "region_id": "25118",
+ "url": "/city/25118/CA/Quartz-Hill"
+ },
+ {
+ "query": "Lake Los Angeles",
+ "name": "Lake Los Angeles",
+ "region_id": "23695",
+ "url": "/city/23695/CA/Lake-Los-Angeles"
+ },
+ {
+ "query": "Sun Village",
+ "name": "Sun Village",
+ "region_id": "35235",
+ "url": "/city/35235/CA/Sun-Village"
+ },
+ {
+ "query": "Desert View Highlands",
+ "name": "Desert View Highlands",
+ "region_id": "22297",
+ "url": "/city/22297/CA/Desert-View-Highlands"
+ },
+ {
+ "query": "La Crescenta",
+ "name": "La Crescenta-Montrose",
+ "region_id": "23645",
+ "url": "/city/23645/CA/La-Crescenta-Montrose"
+ },
+ {
+ "query": "West Carson",
+ "name": "West Carson",
+ "region_id": "26728",
+ "url": "/city/26728/CA/West-Carson"
+ },
+ {
+ "query": "Rancho Dominguez",
+ "name": "West Rancho Dominguez",
+ "region_id": "35544",
+ "url": "/city/35544/CA/West-Rancho-Dominguez"
+ },
+ {
+ "query": "East Rancho Dominguez",
+ "name": "East Rancho Dominguez",
+ "region_id": "32416",
+ "url": "/city/32416/CA/East-Rancho-Dominguez"
+ },
+ {
+ "query": "Athens",
+ "name": "West Athens",
+ "region_id": "26725",
+ "url": "/city/26725/CA/West-Athens"
+ },
+ {
+ "query": "Ladera",
+ "name": "Ladera Ranch",
+ "region_id": "33325",
+ "url": "/city/33325/CA/Ladera-Ranch"
+ },
+ {
+ "query": "View Park-Windsor Hills",
+ "name": "View Park-Windsor Hills",
+ "region_id": "26660",
+ "url": "/city/26660/CA/View-Park-Windsor-Hills"
}
]
}
\ No newline at end of file
diff --git a/public/sales.html b/public/sales.html
index 3a25a4f..d238b9f 100644
--- a/public/sales.html
+++ b/public/sales.html
@@ -31,6 +31,7 @@
</header>
<div class="wrap">
<div class="stat" id="stat">loading…</div>
+ <div id="depthNote" style="font-size:12px;color:var(--muted);margin:4px 0 10px;padding:6px 10px;border-left:2px solid #C8A24B;background:rgba(200,162,75,.06)"></div>
<div class="ctrls">
<input type="text" id="city" placeholder="filter city… e.g. Long Beach">
<input type="number" id="minP" placeholder="min $" step="50000">
@@ -57,7 +58,8 @@ async function load(){
if($('#maxP').value) p.set('maxPrice',$('#maxP').value);
const d=await (await fetch('/api/closed-sales?'+p)).json();
const t=d.total||{};
- $('#stat').innerHTML=`<b>${(t.n||0).toLocaleString()}</b> sold condos · avg <b>${money(t.avg_price)}</b> · ${(t.oldest||'').toString().slice(0,10)} → ${(t.newest||'').toString().slice(0,10)} · <span style="color:var(--muted)">${d.source}</span>`;
+ $('#stat').innerHTML=`<b>${(t.n||0).toLocaleString()}</b> sold condos · avg <b>${money(t.avg_price)}</b> · ${(t.oldest||'').toString().slice(0,10)} → ${(t.newest||'').toString().slice(0,10)} · <span style="color:var(--muted)">${d.source||''}</span>`;
+ if(d.depthNote) $('#depthNote').textContent='ℹ '+d.depthNote;
$('#tbl tbody').innerHTML=(d.rows||[]).slice(0,300).map(r=>`<tr><td>${r.url?`<a class="row" href="${r.url}" target="_blank" rel="noopener noreferrer">${r.address}</a>`:r.address}</td><td>${r.city}</td><td class="n price">${money(r.sold_price)}</td><td class="n" style="color:var(--muted)">${(r.sold_date||'').toString().slice(0,10)}</td></tr>`).join('');
// charts
const bc=d.byCity||[];
diff --git a/scripts/fetch-closed-sales.js b/scripts/fetch-closed-sales.js
index 212c020..8a1d42f 100644
--- a/scripts/fetch-closed-sales.js
+++ b/scripts/fetch-closed-sales.js
@@ -19,6 +19,13 @@ const ROOT = path.join(__dirname, '..');
const pool = new Pool({ host: '/tmp', port: 5432, database: 'cre', user: process.env.USER || 'stevestudio2' });
const MAXR = process.env.CC_MAXREGIONS != null ? +process.env.CC_MAXREGIONS : 12;
const PROBE = process.env.CC_PROBE === '1';
+const NUM_HOMES = 350; // Redfin gis-csv per-query cap
+const CAP_THRESHOLD = NUM_HOMES - 5; // >= this => region is truncated, re-query in price bands
+// Price bands (USD) to union when a region is capped. Open-ended top band catches luxury tail.
+const PRICE_BANDS = [
+ [0, 500000], [500000, 750000], [750000, 1000000],
+ [1000000, 1500000], [1500000, 3000000], [3000000, 0]
+];
function parseCsv(text) {
const lines = text.trim().split('\n'); if (lines.length < 2) return [];
@@ -34,21 +41,57 @@ function parseCsv(text) {
id serial PRIMARY KEY, address text, city text, zip text, sold_price bigint, sold_date date,
beds numeric, baths numeric, sqft int, year_built int, mls text, url text UNIQUE,
source text DEFAULT 'redfin gis-csv (sold)', created_at timestamptz DEFAULT now())`);
+ // Resume ledger: which region+type pairs have been fully swept, so a re-run after a session timeout
+ // skips finished regions and picks up the tail. Idempotent regardless (url is the unique key).
+ await pool.query(`CREATE TABLE IF NOT EXISTS swept_region (
+ region_id text, region_type int, name text, swept_at timestamptz DEFAULT now(),
+ PRIMARY KEY (region_id, region_type))`);
+ const FRESH = process.env.CC_FRESH === '1';
+ if (FRESH) await pool.query('TRUNCATE swept_region');
+ const done = new Set((await pool.query('SELECT region_id, region_type FROM swept_region')).rows
+ .map(r => r.region_id + ':' + r.region_type));
// Region list = resolved city regions (type 6) + neighborhood regions (type 1), from both resolver files.
const load = (f, type) => { try { return (JSON.parse(fs.readFileSync(path.join(ROOT, 'data', f), 'utf8')).regions || []).filter(r => r.region_id).map(r => ({ ...r, region_type: r.region_type || type })); } catch (_) { return []; } };
const seen = new Set();
const regions = [...load('redfin-regions.json', 6), ...load('redfin-neighborhoods.json', 1)]
- .filter(r => { const k = r.region_id + ':' + r.region_type; if (seen.has(k)) return false; seen.add(k); return true; });
+ .filter(r => { const k = r.region_id + ':' + r.region_type; if (seen.has(k)) return false; seen.add(k); return true; })
+ .filter(r => PROBE || !done.has(r.region_id + ':' + r.region_type)); // resume: skip already-swept
const work = (MAXR ? regions.slice(0, MAXR) : regions);
- console.log(`closed-sales: ${work.length} regions${PROBE ? ' (PROBE)' : ''}`);
+ console.log(`closed-sales: ${work.length} regions to sweep${PROBE ? ' (PROBE)' : ''} (${done.size} already done)`);
+
+ // Per-region incremental DB flush so a mid-session Browserbase timeout still persists everything
+ // fetched so far (idempotent on url). Returns # newly inserted.
+ const num = v => { const n = +String(v == null ? '' : v).replace(/[^0-9.]/g, ''); return Number.isFinite(n) && n > 0 ? Math.round(n) : null; };
+ let added = 0, captured = 0;
+ const flush = async (recs, region) => {
+ captured += recs.length;
+ for (const r of recs) {
+ const addr = r['ADDRESS'] || r['Address'], price = num(r['PRICE'] || r['Price']);
+ const url = r['URL'] || r['URL (SEE https://www.redfin.com/buy-a-home/comparative-market-analysis FOR INFO ON PRICING)'];
+ const sold = r['SOLD DATE'] || r['Sold Date'] || r['SOLD_DATE'];
+ if (!addr || !url) continue;
+ try {
+ const ins = await pool.query(`INSERT INTO closed_sale(address,city,zip,sold_price,sold_date,beds,baths,sqft,year_built,mls,url)
+ VALUES($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11) ON CONFLICT(url) DO NOTHING`,
+ [addr, r['CITY'] || r['City'], r['ZIP OR POSTAL CODE'] || r['ZIP'], price,
+ sold && /\d/.test(sold) ? new Date(sold) : null, num(r['BEDS']), num(r['BATHS']),
+ num(r['SQUARE FEET']), num(r['YEAR BUILT']), r['MLS#'] || r['MLS'], url.startsWith('http') ? url : 'https://www.redfin.com' + url]);
+ added += ins.rowCount || 0;
+ } catch (_) {}
+ }
+ if (region) await pool.query('INSERT INTO swept_region(region_id,region_type,name) VALUES($1,$2,$3) ON CONFLICT DO NOTHING',
+ [region.region_id, region.region_type, region.name || region.query]);
+ };
// Candidate sold-filter params to try (Redfin's gis-csv sold mode). First that returns rows wins.
+ // Redfin's free sold feed empirically returns ~5yr depth even with sold_within_days=3650 (10yr) —
+ // the depth is verified honestly AFTER the sweep (see min(sold_date) report); we do NOT label 10yr.
const SOLD_VARIANTS = [
- 's=9&num_homes=350&sold_within_days=3650', // sold last 10yr (3650d)
- 'status=9&sold_within_days=3650&num_homes=350',
- 'sold=1&sold_within_days=3650&num_homes=350',
- 'include=sold-3yr&num_homes=350'
+ `s=9&num_homes=${NUM_HOMES}&sold_within_days=3650`,
+ `status=9&sold_within_days=3650&num_homes=${NUM_HOMES}`,
+ `sold=1&sold_within_days=3650&num_homes=${NUM_HOMES}`,
+ `include=sold-3yr&num_homes=${NUM_HOMES}`
];
let browser, rows = [], soldParam = null;
@@ -61,9 +104,20 @@ function parseCsv(text) {
page.setDefaultTimeout(45000);
await page.goto('https://www.redfin.com/', { waitUntil: 'domcontentloaded' }); await page.waitForTimeout(3000);
+ // in-page fetch of one gis-csv url -> parsed rows (or [] on err). 'price' = optional [min,max] band.
+ const fetchRegion = async (r, soldP, band) => {
+ let extra = '';
+ if (band) { if (band[0] > 0) extra += `&min_price=${band[0]}`; if (band[1] > 0) extra += `&max_price=${band[1]}`; }
+ const url = `https://www.redfin.com/stingray/api/gis-csv?al=1®ion_id=${r.region_id}®ion_type=${r.region_type || 6}&uipt=3&${soldP}${extra}`;
+ const txt = await page.evaluate(async u => { try { const x = await fetch(u); return x.ok ? await x.text() : ('ERR' + x.status); } catch (e) { return 'EXC'; } }, url);
+ if (typeof txt === 'string' && (txt.startsWith('ERR') || txt === 'EXC')) return [];
+ return parseCsv(txt);
+ };
+
// PROBE: find the sold param that returns rows, on the first region.
const r0 = work[0];
- for (const v of SOLD_VARIANTS) {
+ if (!r0) { console.log('nothing to sweep — all resolved regions already in swept_region (CC_FRESH=1 to re-sweep).'); }
+ else for (const v of SOLD_VARIANTS) {
const url = `https://www.redfin.com/stingray/api/gis-csv?al=1®ion_id=${r0.region_id}®ion_type=${r0.region_type}&uipt=3&${v}`;
const txt = await page.evaluate(async u => { try { const r = await fetch(u); return r.ok ? await r.text() : ('ERR' + r.status); } catch (e) { return 'EXC'; } }, url);
const n = (typeof txt === 'string' && txt.startsWith('ERR')) ? 0 : parseCsv(txt).length;
@@ -72,34 +126,39 @@ function parseCsv(text) {
}
if (!soldParam) { console.log('NO sold variant returned rows — Redfin sold feed param needs revisiting.'); }
else if (!PROBE) {
+ let bandedRegions = 0, sweptCount = 0;
for (const r of work) {
- const url = `https://www.redfin.com/stingray/api/gis-csv?al=1®ion_id=${r.region_id}®ion_type=${r.region_type || 6}&uipt=3&${soldParam}`;
- const txt = await page.evaluate(async u => { try { const x = await fetch(u); return x.ok ? await x.text() : 'ERR'; } catch (e) { return 'EXC'; } }, url);
- if (typeof txt === 'string' && (txt === 'ERR' || txt === 'EXC')) continue;
- rows.push(...parseCsv(txt).map(x => ({ ...x, _region: r.name })));
- await page.waitForTimeout(800);
+ let base;
+ try { base = await fetchRegion(r, soldParam); }
+ catch (e) { console.error(' session closed mid-sweep — flushed regions persist, re-run to resume:', e.message); break; }
+ if (base.length < CAP_THRESHOLD) {
+ await flush(base, r); sweptCount++;
+ await page.waitForTimeout(400);
+ continue;
+ }
+ // Region is capped/truncated — re-query in price bands and union (dedupe on url before flush).
+ bandedRegions++;
+ const seenUrl = new Set(); const unioned = [];
+ const keep = arr => { for (const x of arr) { const u = x['URL'] || x['URL (SEE https://www.redfin.com/buy-a-home/comparative-market-analysis FOR INFO ON PRICING)']; if (u && !seenUrl.has(u)) { seenUrl.add(u); unioned.push(x); } } };
+ keep(base);
+ let broke = false;
+ for (const band of PRICE_BANDS) {
+ try { keep(await fetchRegion(r, soldParam, band)); }
+ catch (e) { broke = true; break; }
+ await page.waitForTimeout(350);
+ }
+ await flush(unioned, broke ? null : r); sweptCount++; // only mark done if all bands completed
+ process.stderr.write(` [banded] ${r.name}: base ${base.length} -> ${seenUrl.size} unioned across ${PRICE_BANDS.length} bands${broke ? ' (PARTIAL — not marked done)' : ''}\n`);
+ if (broke) { console.error(' session closed mid-band — re-run to resume.'); break; }
}
+ console.log(`swept ${sweptCount}/${work.length} regions this session (${bandedRegions} banded)`);
}
} catch (e) { console.error('FATAL', e.message); } finally { if (browser) await browser.close().catch(() => {}); }
- // ingest
- const num = v => { const n = +String(v == null ? '' : v).replace(/[^0-9.]/g, ''); return Number.isFinite(n) && n > 0 ? Math.round(n) : null; };
- let added = 0;
- for (const r of rows) {
- const addr = r['ADDRESS'] || r['Address'], price = num(r['PRICE'] || r['Price']), url = r['URL'] || r['URL (SEE https://www.redfin.com/buy-a-home/comparative-market-analysis FOR INFO ON PRICING)'];
- const sold = r['SOLD DATE'] || r['Sold Date'] || r['SOLD_DATE'];
- if (!addr || !url) continue;
- try {
- await pool.query(`INSERT INTO closed_sale(address,city,zip,sold_price,sold_date,beds,baths,sqft,year_built,mls,url)
- VALUES($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11) ON CONFLICT(url) DO NOTHING`,
- [addr, r['CITY'] || r['City'], r['ZIP OR POSTAL CODE'] || r['ZIP'], price,
- sold && /\d/.test(sold) ? new Date(sold) : null, num(r['BEDS']), num(r['BATHS']),
- num(r['SQUARE FEET']), num(r['YEAR BUILT']), r['MLS#'] || r['MLS'], url.startsWith('http') ? url : 'https://www.redfin.com' + url]);
- added++;
- } catch (_) {}
- }
- const total = (await pool.query('SELECT count(*)::int n FROM closed_sale')).rows[0].n;
- console.log(`\nsold param = ${soldParam || 'NONE'} | captured ${rows.length} rows, +${added} new | closed_sale total ${total}`);
+ const depth = (await pool.query(
+ 'SELECT count(*)::int n, min(sold_date) oldest, max(sold_date) newest, count(distinct city)::int cities FROM closed_sale')).rows[0];
+ console.log(`\nsold param = ${soldParam || 'NONE'} | captured ${captured} rows, +${added} new | closed_sale total ${depth.n}`);
+ console.log(`HONEST DEPTH: ${depth.oldest ? depth.oldest.toISOString().slice(0, 10) : '?'} -> ${depth.newest ? depth.newest.toISOString().slice(0, 10) : '?'} across ${depth.cities} cities (Redfin free sold feed ~5yr; NOT 10yr).`);
console.log('cost: ~$0.04-0.10 (1 Browserbase session).');
await pool.end();
})();
diff --git a/scripts/resolve-redfin-neighborhoods.js b/scripts/resolve-redfin-neighborhoods.js
index 79dc8f3..a21dcbc 100644
--- a/scripts/resolve-redfin-neighborhoods.js
+++ b/scripts/resolve-redfin-neighborhoods.js
@@ -5,12 +5,27 @@ const fs = require('fs');
const path = require('path');
const { chromium } = require('playwright-core');
const Browserbase = require('@browserbasehq/sdk').default;
+const { LA_NEIGHBORHOODS } = require('./sources/la-county');
const env = fs.readFileSync(process.env.HOME + '/.claude/skills/browserbase/.env', 'utf8');
const get = (t, k) => (t.match(new RegExp('^' + k + '=(.*)$', 'm')) || [])[1]?.replace(/['"]/g, '').trim();
const KEY = get(env, 'BROWSERBASE_API_KEY'), PROJECT = get(env, 'BROWSERBASE_PROJECT_ID');
-const HOODS = (process.env.CC_HOODS || 'Van Nuys,Reseda,North Hollywood,Sherman Oaks,Encino,Woodland Hills,Northridge,Canoga Park,Studio City,Echo Park,Highland Park,Koreatown,Venice,Mar Vista,Pacific Palisades,San Pedro').split(',').map(s => s.trim());
+
+// CC_HOODS=<csv> overrides; CC_ALL=1 = full LA_NEIGHBORHOODS set (title-cased, deduped on alias spellings).
+const titleCase = s => s.replace(/\b\w/g, c => c.toUpperCase());
+const FULL_HOODS = [...new Set(LA_NEIGHBORHOODS.map(titleCase))];
+const HOODS = (process.env.CC_HOODS ? process.env.CC_HOODS.split(',')
+ : (process.env.CC_ALL === '1' ? FULL_HOODS
+ : 'Van Nuys,Reseda,North Hollywood,Sherman Oaks,Encino,Woodland Hills,Northridge,Canoga Park,Studio City,Echo Park,Highland Park,Koreatown,Venice,Mar Vista,Pacific Palisades,San Pedro'.split(','))).map(s => s.trim()).filter(Boolean);
+const HOODS_FILE = path.join(__dirname, '..', 'data', 'redfin-neighborhoods.json');
(async () => {
+ let prior = { regions: [] };
+ try { prior = JSON.parse(fs.readFileSync(HOODS_FILE, 'utf8')); } catch (_) {}
+ const haveIds = new Set((prior.regions || []).filter(r => r.region_id).map(r => String(r.region_id)));
+ const haveQueries = new Set((prior.regions || []).filter(r => r.region_id).map(r => (r.query || '').toLowerCase()));
+ const todo = HOODS.filter(h => !haveQueries.has(h.toLowerCase()));
+ process.stderr.write(`resolve-hoods: ${HOODS.length} requested, ${todo.length} new (skipping ${HOODS.length - todo.length} already resolved)\n`);
+
const regions = [];
let browser, session;
try {
@@ -23,7 +38,7 @@ const HOODS = (process.env.CC_HOODS || 'Van Nuys,Reseda,North Hollywood,Sherman
await page.goto('https://www.redfin.com/', { waitUntil: 'domcontentloaded' });
await page.waitForTimeout(3000);
- for (const hood of HOODS) {
+ for (const hood of todo) {
const r = await page.evaluate(async (q) => {
try {
const resp = await fetch('https://www.redfin.com/stingray/do/location-autocomplete?location=' +
@@ -51,8 +66,16 @@ const HOODS = (process.env.CC_HOODS || 'Van Nuys,Reseda,North Hollywood,Sherman
} catch (e) { process.stderr.write('ERR ' + e.message + '\n'); }
finally { if (browser) try { await browser.close(); } catch (_) {} }
- fs.writeFileSync(path.join(__dirname, '..', 'data', 'redfin-neighborhoods.json'),
- JSON.stringify({ resolved_at: new Date().toISOString(), regions }, null, 2));
- console.log(JSON.stringify({ requested: HOODS.length, resolved: regions.filter(r => r.region_id).length,
- missing: regions.filter(r => !r.region_id).map(r => r.query) }));
+ const merged = [...(prior.regions || [])];
+ let appended = 0, miss = [];
+ for (const r of regions) {
+ if (!r.region_id) { miss.push(r.query); continue; }
+ if (haveIds.has(String(r.region_id))) continue;
+ haveIds.add(String(r.region_id)); merged.push(r); appended++;
+ }
+ const resolvedTotal = merged.filter(r => r.region_id).length;
+ fs.writeFileSync(HOODS_FILE,
+ JSON.stringify({ resolved_at: new Date().toISOString(), count: resolvedTotal, regions: merged }, null, 2));
+ console.log(JSON.stringify({ requested: HOODS.length, newly_queried: todo.length,
+ appended, resolved_total: resolvedTotal, missing: miss }));
})();
diff --git a/scripts/resolve-redfin-regions.js b/scripts/resolve-redfin-regions.js
index e10a3eb..02d02d3 100644
--- a/scripts/resolve-redfin-regions.js
+++ b/scripts/resolve-redfin-regions.js
@@ -7,13 +7,29 @@ const fs = require('fs');
const path = require('path');
const { chromium } = require('playwright-core');
const Browserbase = require('@browserbasehq/sdk').default;
-const { NAV_HUBS } = require('./sources/la-county');
+const { NAV_HUBS, INCORPORATED, UNINCORPORATED } = require('./sources/la-county');
const env = fs.readFileSync(process.env.HOME + '/.claude/skills/browserbase/.env', 'utf8');
const get = (t, k) => (t.match(new RegExp('^' + k + '=(.*)$', 'm')) || [])[1]?.replace(/['"]/g, '').trim();
const KEY = get(env, 'BROWSERBASE_API_KEY'), PROJECT = get(env, 'BROWSERBASE_PROJECT_ID');
-const CITIES = (process.env.CC_CITIES ? process.env.CC_CITIES.split(',') : NAV_HUBS).map(s => s.trim()).filter(Boolean);
+
+// CC_CITIES=<csv> overrides; CC_ALL=1 = full county place set (88 incorporated + unincorporated
+// communities), title-cased for the autocomplete query; default = the NAV_HUBS investment set.
+const titleCase = s => s.replace(/\b\w/g, c => c.toUpperCase());
+const FULL_PLACES = [...new Set([...INCORPORATED, ...UNINCORPORATED].map(titleCase))];
+const CITIES = (process.env.CC_CITIES ? process.env.CC_CITIES.split(',')
+ : (process.env.CC_ALL === '1' ? FULL_PLACES : NAV_HUBS)).map(s => s.trim()).filter(Boolean);
+const REGIONS_FILE = path.join(__dirname, '..', 'data', 'redfin-regions.json');
(async () => {
+ // Load existing so we APPEND + DEDUPE by region_id rather than overwrite (cities that don't resolve
+ // are logged + skipped, never faked).
+ let prior = { regions: [] };
+ try { prior = JSON.parse(fs.readFileSync(REGIONS_FILE, 'utf8')); } catch (_) {}
+ const haveIds = new Set((prior.regions || []).filter(r => r.region_id).map(r => String(r.region_id)));
+ const haveQueries = new Set((prior.regions || []).filter(r => r.region_id).map(r => (r.query || '').toLowerCase()));
+ const todo = CITIES.filter(c => !haveQueries.has(c.toLowerCase()));
+ process.stderr.write(`resolve-regions: ${CITIES.length} requested, ${todo.length} new (skipping ${CITIES.length - todo.length} already resolved)\n`);
+
const regions = [];
let browser, session;
try {
@@ -26,7 +42,7 @@ const CITIES = (process.env.CC_CITIES ? process.env.CC_CITIES.split(',') : NAV_H
await page.goto('https://www.redfin.com/', { waitUntil: 'domcontentloaded' });
await page.waitForTimeout(3000);
- for (const city of CITIES) {
+ for (const city of todo) {
const r = await page.evaluate(async (q) => {
try {
const resp = await fetch('https://www.redfin.com/stingray/do/location-autocomplete?location=' +
@@ -51,9 +67,17 @@ const CITIES = (process.env.CC_CITIES ? process.env.CC_CITIES.split(',') : NAV_H
} catch (e) { process.stderr.write('ERR ' + e.message + '\n'); }
finally { if (browser) try { await browser.close(); } catch (_) {} }
- const resolved = regions.filter(r => r.region_id);
- fs.writeFileSync(path.join(__dirname, '..', 'data', 'redfin-regions.json'),
- JSON.stringify({ resolved_at: new Date().toISOString(), count: resolved.length, regions }, null, 2));
- console.log(JSON.stringify({ requested: CITIES.length, resolved: resolved.length,
- missing: regions.filter(r => !r.region_id).map(r => r.query) }));
+ // MERGE: keep all prior entries, append newly-resolved ones whose region_id we don't already have.
+ const merged = [...(prior.regions || [])];
+ let appended = 0, miss = [];
+ for (const r of regions) {
+ if (!r.region_id) { miss.push(r.query); continue; }
+ if (haveIds.has(String(r.region_id))) continue;
+ haveIds.add(String(r.region_id)); merged.push(r); appended++;
+ }
+ const resolvedTotal = merged.filter(r => r.region_id).length;
+ fs.writeFileSync(REGIONS_FILE,
+ JSON.stringify({ resolved_at: new Date().toISOString(), count: resolvedTotal, regions: merged }, null, 2));
+ console.log(JSON.stringify({ requested: CITIES.length, newly_queried: todo.length,
+ appended, resolved_total: resolvedTotal, missing: miss }));
})();
diff --git a/scripts/serve.js b/scripts/serve.js
index a07080a..bb64c90 100644
--- a/scripts/serve.js
+++ b/scripts/serve.js
@@ -368,7 +368,9 @@ app.get('/api/closed-sales', async (req, res) => {
const byCity = await q(`SELECT city, count(*)::int n, round(avg(sold_price))::bigint avg_price FROM closed_sale ${w} GROUP BY city ORDER BY n DESC LIMIT 16`, args);
const byYear = await q(`SELECT extract(year FROM sold_date)::int yr, count(*)::int n, round(avg(sold_price))::bigint avg_price FROM closed_sale ${w} AND sold_date IS NOT NULL GROUP BY 1 ORDER BY 1`, args);
const tot = (await q(`SELECT count(*)::int n, round(avg(sold_price))::bigint avg_price, min(sold_date) oldest, max(sold_date) newest FROM closed_sale ${w}`, args))[0];
- res.json({ rows, byCity, byYear, total: tot, source: 'Redfin sold feed (gis-csv, ~5yr) — area market comps' });
+ res.json({ rows, byCity, byYear, total: tot,
+ source: 'Redfin public sold feed (gis-csv)',
+ depthNote: 'Redfin public sold feed (~5yr depth, 2021+); full 10-yr history requires a paid records source (ATTOM / county recorder).' });
} catch (e) { res.status(502).json({ error: String(e.message).split('\n')[0], rows: [] }); }
});
← 5287876 CRCP: sortable columns on broker + firm tables (click any he
·
back to Commercialrealestate
·
CRE: sortable columns on /sales.html comps table (address/ci 4d1c2a9 →