← back to Nationalrealestate
USRE: geo-search now resolves ZIP (zip_county crosswalk) + street address (Census geocoder); rolled site-wide (pill on map/content, inline in grid top-bar) (v0.13.0)
e08091bee055bfe4bfb7cb370af44e1ae907ca77 · 2026-07-26 21:09:05 -0700 · Steve
Files touched
M package-lock.jsonM package.jsonM public/admin.htmlM public/brokers.htmlM public/commercial-brokers.htmlM public/commercial-feed.htmlM public/commercial-property.htmlM public/commercial.htmlM public/compare.htmlM public/geo-search.cssM public/geo-search.jsM public/listings.htmlM public/market.htmlM public/markets.htmlM public/property.htmlM public/sources.htmlM public/watchlist.htmlA src/ingest/zip_county.tsM src/server/index.ts
Diff
commit e08091bee055bfe4bfb7cb370af44e1ae907ca77
Author: Steve <steve@designerwallcoverings.com>
Date: Sun Jul 26 21:09:05 2026 -0700
USRE: geo-search now resolves ZIP (zip_county crosswalk) + street address (Census geocoder); rolled site-wide (pill on map/content, inline in grid top-bar) (v0.13.0)
---
package-lock.json | 4 +--
package.json | 5 ++--
public/admin.html | 2 ++
public/brokers.html | 2 ++
public/commercial-brokers.html | 2 ++
public/commercial-feed.html | 2 ++
public/commercial-property.html | 2 ++
public/commercial.html | 2 ++
public/compare.html | 2 ++
public/geo-search.css | 48 ++++++++++++++++++++-----------
public/geo-search.js | 62 ++++++++++++++++++++++++++---------------
public/listings.html | 2 ++
public/market.html | 2 ++
public/markets.html | 2 ++
public/property.html | 2 ++
public/sources.html | 2 ++
public/watchlist.html | 2 ++
src/ingest/zip_county.ts | 50 +++++++++++++++++++++++++++++++++
src/server/index.ts | 32 +++++++++++++++++++++
19 files changed, 183 insertions(+), 44 deletions(-)
diff --git a/package-lock.json b/package-lock.json
index d793155..68254b3 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,12 +1,12 @@
{
"name": "nationalrealestate",
- "version": "0.12.0",
+ "version": "0.13.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "nationalrealestate",
- "version": "0.12.0",
+ "version": "0.13.0",
"dependencies": {
"better-sqlite3": "^13.0.1",
"dotenv": "^16.4.5",
diff --git a/package.json b/package.json
index d3eefc9..35be955 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "nationalrealestate",
- "version": "0.12.0",
+ "version": "0.13.0",
"private": true,
"type": "module",
"description": "USRealEstate — national U.S. residential market explorer. Free-data v1 (Redfin Data Center, Zillow Research, Census ACS, FHFA). Internal analyst tool behind basic auth.",
@@ -26,7 +26,8 @@
"ingest:commercial": "tsx src/ingest/commercial/engine.ts",
"ingest:commercial-briefs": "tsx src/ingest/commercial/briefs.ts",
"ingest:places": "tsx src/ingest/places/seed.ts",
- "loop": "tsx src/jobs/hourly_loop.ts"
+ "loop": "tsx src/jobs/hourly_loop.ts",
+ "ingest:zipcounty": "tsx src/ingest/zip_county.ts"
},
"dependencies": {
"better-sqlite3": "^13.0.1",
diff --git a/public/admin.html b/public/admin.html
index 8459c79..a389bb5 100644
--- a/public/admin.html
+++ b/public/admin.html
@@ -35,6 +35,7 @@
.empty{color:var(--muted);padding:18px;text-align:center;}
</style>
<link rel="stylesheet" href="/nav-drawer.css">
+<link rel="stylesheet" href="/geo-search.css">
</head>
<body>
<div id="topbar">
@@ -104,5 +105,6 @@ loadRuns();
setInterval(loadRuns, 30000);
</script>
<script src="/nav-drawer.js"></script>
+<script src="/geo-search.js"></script>
</body>
</html>
diff --git a/public/brokers.html b/public/brokers.html
index 7489d64..74ba0ce 100644
--- a/public/brokers.html
+++ b/public/brokers.html
@@ -54,6 +54,7 @@
.usre-fd .fd-brokers span{color:var(--muted);font-size:12px;white-space:nowrap;}
</style>
<link rel="stylesheet" href="/nav-drawer.css">
+<link rel="stylesheet" href="/geo-search.css">
</head>
<body>
<div id="topbar">
@@ -244,5 +245,6 @@ loadStats();
loadPage();
</script>
<script src="/nav-drawer.js"></script>
+<script src="/geo-search.js"></script>
</body>
</html>
diff --git a/public/commercial-brokers.html b/public/commercial-brokers.html
index 187d8ef..4a16f44 100644
--- a/public/commercial-brokers.html
+++ b/public/commercial-brokers.html
@@ -32,6 +32,7 @@
#loading{padding:40px;text-align:center;color:var(--muted);}
</style>
<link rel="stylesheet" href="/nav-drawer.css">
+<link rel="stylesheet" href="/geo-search.css">
</head>
<body>
<div id="topbar">
@@ -98,5 +99,6 @@ $('#q').oninput=()=>{state.q=$('#q').value;clearTimeout(window._t);window._t=set
load();
</script>
<script src="/nav-drawer.js"></script>
+<script src="/geo-search.js"></script>
</body>
</html>
diff --git a/public/commercial-feed.html b/public/commercial-feed.html
index 4b340f7..94bebfa 100644
--- a/public/commercial-feed.html
+++ b/public/commercial-feed.html
@@ -36,6 +36,7 @@
#loading{padding:40px;text-align:center;color:var(--muted);}
</style>
<link rel="stylesheet" href="/nav-drawer.css">
+<link rel="stylesheet" href="/geo-search.css">
</head>
<body>
<div id="topbar">
@@ -114,5 +115,6 @@ async function load(){
(async()=>{ await loadTypes(); await loadCities(); await load(); })();
</script>
<script src="/nav-drawer.js"></script>
+<script src="/geo-search.js"></script>
</body>
</html>
diff --git a/public/commercial-property.html b/public/commercial-property.html
index f47933c..b54b33e 100644
--- a/public/commercial-property.html
+++ b/public/commercial-property.html
@@ -31,6 +31,7 @@
.back{font-size:13px;color:var(--muted);text-decoration:none;} .back:hover{color:var(--gold);}
</style>
<link rel="stylesheet" href="/nav-drawer.css">
+<link rel="stylesheet" href="/geo-search.css">
</head>
<body>
<div id="topbar">
@@ -86,5 +87,6 @@ function dstr(d){return d?new Date(d).toLocaleDateString(undefined,{year:'numeri
})();
</script>
<script src="/nav-drawer.js"></script>
+<script src="/geo-search.js"></script>
</body>
</html>
diff --git a/public/commercial.html b/public/commercial.html
index 0cac07b..4f06439 100644
--- a/public/commercial.html
+++ b/public/commercial.html
@@ -41,6 +41,7 @@
#loading{padding:50px;text-align:center;color:var(--muted);}
</style>
<link rel="stylesheet" href="/nav-drawer.css">
+<link rel="stylesheet" href="/geo-search.css">
</head>
<body>
<div id="topbar">
@@ -133,5 +134,6 @@ $('#sort').onchange=()=>{state.sort=$('#sort').value;localStorage.setItem('cre_s
(async()=>{ await loadTypes(); await loadCities(); await load(); })();
</script>
<script src="/nav-drawer.js"></script>
+<script src="/geo-search.js"></script>
</body>
</html>
diff --git a/public/compare.html b/public/compare.html
index ea3daa5..2c57142 100644
--- a/public/compare.html
+++ b/public/compare.html
@@ -38,6 +38,7 @@
.empty{color:var(--muted);padding:50px;text-align:center;}
</style>
<link rel="stylesheet" href="/nav-drawer.css">
+<link rel="stylesheet" href="/geo-search.css">
</head>
<body>
<div id="topbar">
@@ -196,5 +197,6 @@ async function render(){
loadRegions().then(render);
</script>
<script src="/nav-drawer.js"></script>
+<script src="/geo-search.js"></script>
</body>
</html>
diff --git a/public/geo-search.css b/public/geo-search.css
index a8f3059..3b5eac7 100644
--- a/public/geo-search.css
+++ b/public/geo-search.css
@@ -1,28 +1,42 @@
-/* geo-search.css — top-center location search pill (pairs with geo-search.js).
- * Inherits the host page's --bg/--panel/--line/--fg/--muted/--gold CSS vars. */
-#geoSearch{position:fixed;top:12px;left:50%;transform:translateX(-50%);z-index:1100;width:min(440px,86vw);}
-#geoSearchBox{display:flex;align-items:center;gap:8px;height:44px;padding:0 12px;
- background:rgba(17,21,29,.96);border:1px solid var(--line,#222936);border-radius:22px;
- backdrop-filter:blur(6px);box-shadow:0 2px 14px rgba(0,0,0,.5);}
-#geoSearchBox:focus-within{border-color:var(--gold,#c8a24b);}
-#geoSearchIc{color:var(--gold,#c8a24b);font-size:16px;line-height:1;}
-#geoSearchInput{flex:1;min-width:0;background:none;border:0;outline:0;color:var(--fg,#e8ecf2);
+/* geo-search.css — universal location search (pairs with geo-search.js).
+ * Two modes: .geo-pill (fixed top-center, on map/content pages) and
+ * .geo-inline (sits inside the grid's .cg-top bar). Inherits host --bg/--line/--fg/--muted/--gold. */
+
+/* shared box */
+.geo-box{display:flex;align-items:center;gap:8px;height:40px;padding:0 12px;
+ background:rgba(17,21,29,.96);border:1px solid var(--line,#222936);border-radius:22px;}
+.geo-box:focus-within{border-color:var(--gold,#c8a24b);}
+.geo-ic{color:var(--gold,#c8a24b);font-size:16px;line-height:1;}
+.geo-input{flex:1;min-width:0;background:none;border:0;outline:0;color:var(--fg,#e8ecf2);
font:14px/1.4 -apple-system,Segoe UI,Roboto,sans-serif;}
-#geoSearchInput::placeholder{color:var(--muted,#8a93a3);}
-#geoSearchClear{background:none;border:0;color:var(--muted,#8a93a3);cursor:pointer;font-size:13px;padding:2px 4px;}
-#geoSearchClear:hover{color:var(--fg,#e8ecf2);}
-#geoSearchResults{display:none;margin-top:6px;max-height:60vh;overflow-y:auto;
+.geo-input::placeholder{color:var(--muted,#8a93a3);}
+.geo-clear{background:none;border:0;color:var(--muted,#8a93a3);cursor:pointer;font-size:13px;padding:2px 4px;}
+.geo-clear:hover{color:var(--fg,#e8ecf2);}
+
+/* results dropdown */
+.geo-results{display:none;max-height:60vh;overflow-y:auto;z-index:1200;
background:rgba(17,21,29,.98);border:1px solid var(--line,#222936);border-radius:12px;
box-shadow:0 8px 24px rgba(0,0,0,.55);}
-#geoSearchResults.open{display:block;}
+.geo-results.open{display:block;}
.geo-item{display:flex;align-items:center;gap:10px;padding:10px 14px;text-decoration:none;
color:var(--fg,#e8ecf2);border-bottom:1px solid var(--line,#222936);}
.geo-item:last-child{border-bottom:0;}
.geo-item:hover,.geo-item.active{background:rgba(200,162,75,.12);}
-.geo-name{flex:1;font-size:14px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;}
+.geo-name{flex:1;min-width:0;font-size:14px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;}
+.geo-subline{display:block;font-size:11px;color:var(--muted,#8a93a3);white-space:nowrap;overflow:hidden;text-overflow:ellipsis;}
.geo-state{font-size:11px;color:var(--muted,#8a93a3);font-weight:700;font-variant-numeric:tabular-nums;}
.geo-badge{font-size:10px;text-transform:uppercase;letter-spacing:.5px;color:var(--gold,#c8a24b);
border:1px solid var(--line,#222936);border-radius:10px;padding:2px 7px;white-space:nowrap;}
.geo-empty{padding:12px 14px;color:var(--muted,#8a93a3);font-size:13px;line-height:1.5;}
-/* mobile: clear the fixed upper-left hamburger, don't center under it */
-@media(max-width:820px){#geoSearch{left:64px;right:12px;width:auto;transform:none;}}
+
+/* ── pill mode: fixed top-center (map + content pages) ── */
+.geo-pill{position:fixed;top:12px;left:50%;transform:translateX(-50%);z-index:1100;width:min(440px,86vw);}
+.geo-pill .geo-box{height:44px;backdrop-filter:blur(6px);box-shadow:0 2px 14px rgba(0,0,0,.5);}
+.geo-pill .geo-results{margin-top:6px;}
+@media(max-width:820px){.geo-pill{left:64px;right:12px;width:auto;transform:none;}} /* clear the hamburger */
+
+/* ── inline mode: first control inside the grid's .cg-top bar ── */
+.geo-inline{position:relative;flex:0 0 auto;}
+.geo-inline .geo-box{width:260px;max-width:42vw;background:var(--cg-card,#161b22);}
+.geo-inline .geo-results{position:absolute;top:calc(100% + 6px);left:0;min-width:340px;max-width:80vw;}
+@media(max-width:820px){.geo-inline .geo-box{width:180px;}}
diff --git a/public/geo-search.js b/public/geo-search.js
index b91ed06..5038f81 100644
--- a/public/geo-search.js
+++ b/public/geo-search.js
@@ -1,40 +1,42 @@
-/* geo-search.js — top-center location search. Type a county / city / state,
- * get a typeahead of markets (via /api/geo-search), and jump to that market page.
- * National coverage from the region table. Include on any page + a matching CSS link. */
+/* geo-search.js — universal top location search. Type an address / ZIP / city / county /
+ * state, get a typeahead (via /api/geo-search), and jump to that market page.
+ * Mounts two ways so it fits every page:
+ * - grid pages (have .cg-shell): INLINE as the first control in the grid's .cg-top bar
+ * - everything else: a fixed top-center pill
+ * Include on any page + the matching geo-search.css. */
(function () {
- function init() {
- if (document.getElementById('geoSearch')) return;
- const wrap = document.createElement('div'); wrap.id = 'geoSearch';
- wrap.innerHTML =
- '<div id="geoSearchBox"><span id="geoSearchIc">⌕</span>' +
- '<input id="geoSearchInput" type="text" autocomplete="off" spellcheck="false" ' +
- 'placeholder="Search a county, city, or state…" aria-label="Location search">' +
- '<button id="geoSearchClear" type="button" aria-label="Clear" style="display:none">✕</button></div>' +
- '<div id="geoSearchResults"></div>';
- document.body.appendChild(wrap);
- const input = wrap.querySelector('#geoSearchInput');
- const results = wrap.querySelector('#geoSearchResults');
- const clear = wrap.querySelector('#geoSearchClear');
+ const esc = s => String(s == null ? '' : s).replace(/&/g, '&').replace(/</g, '<').replace(/"/g, '"');
+ const badge = ty => ({ county: 'County', metro: 'Metro', state: 'State', zip: 'ZIP', address: 'Address' }[ty] || ty);
+
+ function build(host, mode) {
+ host.className = 'geo-host geo-' + mode;
+ host.innerHTML =
+ '<div class="geo-box"><span class="geo-ic">⌕</span>' +
+ '<input class="geo-input" type="text" autocomplete="off" spellcheck="false" ' +
+ 'placeholder="Search address, ZIP, city, or county…" aria-label="Location search">' +
+ '<button class="geo-clear" type="button" aria-label="Clear" style="display:none">✕</button></div>' +
+ '<div class="geo-results"></div>';
+ const input = host.querySelector('.geo-input');
+ const results = host.querySelector('.geo-results');
+ const clear = host.querySelector('.geo-clear');
let items = [], active = -1, t = null, lastQ = '';
const close = () => { results.innerHTML = ''; results.classList.remove('open'); items = []; active = -1; };
const go = u => { if (u) location.href = u; };
- const badge = ty => ty === 'county' ? 'County' : ty === 'metro' ? 'Metro' : 'State';
- const esc = s => String(s == null ? '' : s).replace(/&/g, '&').replace(/</g, '<').replace(/"/g, '"');
function render(rows) {
items = rows;
if (!rows.length) {
const zipish = /^\d{5}$/.test(lastQ), addrish = /^\d+\s+\S/.test(lastQ);
results.innerHTML = '<div class="geo-empty">' +
- (zipish || addrish
- ? 'No place match. ZIP / street-address lookup is per-county — open a market, then use its property search.'
+ (zipish ? `No county found for ZIP ${esc(lastQ)}.`
+ : addrish ? 'Address not recognized — try including city + state.'
: 'No matches.') + '</div>';
results.classList.add('open'); return;
}
results.innerHTML = rows.map((r, i) =>
`<a class="geo-item" data-i="${i}" href="${esc(r.url)}">` +
- `<span class="geo-name">${esc(r.name)}</span>` +
+ `<span class="geo-name">${esc(r.name)}${r.sub ? `<span class="geo-subline">${esc(r.sub)}</span>` : ''}</span>` +
(r.state ? `<span class="geo-state">${esc(r.state)}</span>` : '') +
`<span class="geo-badge">${badge(r.type)}</span></a>`).join('');
results.classList.add('open'); active = -1;
@@ -56,7 +58,7 @@
clear.style.display = q ? 'block' : 'none';
clearTimeout(t);
if (q.length < 2) { close(); return; }
- t = setTimeout(() => search(q), 160);
+ t = setTimeout(() => search(q), 180);
});
input.addEventListener('keydown', e => {
if (e.key === 'ArrowDown') { e.preventDefault(); setActive(active + 1); }
@@ -65,7 +67,21 @@
else if (e.key === 'Escape') { close(); input.blur(); }
});
clear.addEventListener('click', () => { input.value = ''; clear.style.display = 'none'; close(); input.focus(); });
- document.addEventListener('click', e => { if (!wrap.contains(e.target)) close(); });
+ document.addEventListener('click', e => { if (!host.contains(e.target)) close(); });
+ }
+
+ function init() {
+ if (document.querySelector('.geo-host')) return;
+ const mountPill = () => { const h = document.createElement('div'); document.body.appendChild(h); build(h, 'pill'); };
+ if (document.querySelector('.cg-shell')) {
+ // grid page: wait for CrcpGrid to build .cg-top, then mount inline as its first control
+ let tries = 0;
+ const iv = setInterval(() => {
+ const top = document.querySelector('.cg-top');
+ if (top) { clearInterval(iv); const h = document.createElement('div'); top.insertBefore(h, top.firstChild); build(h, 'inline'); }
+ else if (++tries > 80) { clearInterval(iv); mountPill(); } // fallback if grid never mounts
+ }, 100);
+ } else mountPill();
}
if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', init); else init();
})();
diff --git a/public/listings.html b/public/listings.html
index 8ac5959..42627f5 100644
--- a/public/listings.html
+++ b/public/listings.html
@@ -40,6 +40,7 @@
.src-tag{font-family:ui-monospace,Menlo,monospace;font-size:11px;color:var(--muted);}
</style>
<link rel="stylesheet" href="/nav-drawer.css">
+<link rel="stylesheet" href="/geo-search.css">
</head>
<body>
<div id="topbar">
@@ -188,5 +189,6 @@ loadFilters();
load();
</script>
<script src="/nav-drawer.js"></script>
+<script src="/geo-search.js"></script>
</body>
</html>
diff --git a/public/market.html b/public/market.html
index 5657600..2d78873 100644
--- a/public/market.html
+++ b/public/market.html
@@ -42,6 +42,7 @@
.u-legend .u-label{color:var(--muted);}
</style>
<link rel="stylesheet" href="/nav-drawer.css">
+<link rel="stylesheet" href="/geo-search.css">
</head>
<body>
<div id="topbar">
@@ -219,5 +220,6 @@ function chart(elId, entries, fmt){
})();
</script>
<script src="/nav-drawer.js"></script>
+<script src="/geo-search.js"></script>
</body>
</html>
diff --git a/public/markets.html b/public/markets.html
index b61ab53..29329f1 100644
--- a/public/markets.html
+++ b/public/markets.html
@@ -29,6 +29,7 @@
.score{font-weight:700;color:var(--gold);}
</style>
<link rel="stylesheet" href="/nav-drawer.css">
+<link rel="stylesheet" href="/geo-search.css">
</head>
<body>
<div id="topbar">
@@ -162,5 +163,6 @@ document.getElementById('exportcsv').addEventListener('click', e => {
load(TYPE);
</script>
<script src="/nav-drawer.js"></script>
+<script src="/geo-search.js"></script>
</body>
</html>
diff --git a/public/property.html b/public/property.html
index 936e0a4..c3ef4f3 100644
--- a/public/property.html
+++ b/public/property.html
@@ -62,6 +62,7 @@
#foot{margin-top:22px;color:var(--muted);font-size:11px;}
</style>
<link rel="stylesheet" href="/nav-drawer.css">
+<link rel="stylesheet" href="/geo-search.css">
</head>
<body>
<div id="topbar">
@@ -354,5 +355,6 @@ const loc = qs.get('loc');
if (loc) load(loc);
</script>
<script src="/nav-drawer.js"></script>
+<script src="/geo-search.js"></script>
</body>
</html>
diff --git a/public/sources.html b/public/sources.html
index 3cf067d..3f6b0a9 100644
--- a/public/sources.html
+++ b/public/sources.html
@@ -32,6 +32,7 @@
.foot{color:var(--muted);font-size:12px;margin-top:26px;font-style:italic;}
</style>
<link rel="stylesheet" href="/nav-drawer.css">
+<link rel="stylesheet" href="/geo-search.css">
</head>
<body>
<div id="topbar">
@@ -129,5 +130,6 @@ const int = n => (+n).toLocaleString();
})();
</script>
<script src="/nav-drawer.js"></script>
+<script src="/geo-search.js"></script>
</body>
</html>
diff --git a/public/watchlist.html b/public/watchlist.html
index 41a16d4..8be9c95 100644
--- a/public/watchlist.html
+++ b/public/watchlist.html
@@ -38,6 +38,7 @@
.empty{color:var(--muted);padding:18px;text-align:center;}
</style>
<link rel="stylesheet" href="/nav-drawer.css">
+<link rel="stylesheet" href="/geo-search.css">
</head>
<body>
<div id="topbar">
@@ -161,5 +162,6 @@ async function loadAlerts(){
loadRegions().then(()=>{ loadList(); loadAlerts(); });
</script>
<script src="/nav-drawer.js"></script>
+<script src="/geo-search.js"></script>
</body>
</html>
diff --git a/src/ingest/zip_county.ts b/src/ingest/zip_county.ts
new file mode 100644
index 0000000..1b4cd85
--- /dev/null
+++ b/src/ingest/zip_county.ts
@@ -0,0 +1,50 @@
+/**
+ * zip_county — national ZIP (ZCTA) -> primary county FIPS crosswalk that powers ZIP
+ * lookup in the top location search (/api/geo-search). Source: Census 2020 ZCTA-to-county
+ * relationship file (pipe-delimited, keyless, free). A ZIP can span counties; we keep the
+ * county with the largest land overlap (AREALAND_PART). Idempotent — rebuilds each run.
+ * Run: `npm run ingest:zipcounty` (local) or on prod after deploy.
+ */
+import { query, pool } from '../../db/pool.ts';
+
+const SRC = 'https://www2.census.gov/geo/docs/maps-data/data/rel2020/zcta520/tab20_zcta520_county20_natl.txt';
+
+async function main() {
+ console.log('[zip_county] downloading Census ZCTA→county relationship file…');
+ const res = await fetch(SRC);
+ if (!res.ok) throw new Error('download failed: HTTP ' + res.status);
+ const text = await res.text();
+ const lines = text.split('\n');
+ // fields (0-based): 1=GEOID_ZCTA5 (zip), 9=GEOID_COUNTY (fips), 16=AREALAND_PART
+ const best = new Map<string, { fips: string; area: number }>();
+ for (let i = 1; i < lines.length; i++) {
+ const f = lines[i].split('|');
+ if (f.length < 17) continue;
+ const zip = (f[1] || '').trim();
+ const fips = (f[9] || '').trim();
+ const area = Number(f[16] || 0);
+ if (!/^\d{5}$/.test(zip) || !/^\d{5}$/.test(fips)) continue;
+ const cur = best.get(zip);
+ if (!cur || area > cur.area) best.set(zip, { fips, area });
+ }
+ console.log(`[zip_county] parsed ${best.size} unique ZIPs`);
+ await query(`CREATE TABLE IF NOT EXISTS zip_county (zip text PRIMARY KEY, county_fips text NOT NULL)`);
+ await query(`TRUNCATE zip_county`);
+ const entries = [...best.entries()];
+ const CHUNK = 1000;
+ for (let i = 0; i < entries.length; i += CHUNK) {
+ const slice = entries.slice(i, i + CHUNK);
+ const vals: string[] = [];
+ const params: string[] = [];
+ slice.forEach(([zip, v], j) => { vals.push(`($${j * 2 + 1},$${j * 2 + 2})`); params.push(zip, v.fips); });
+ await query(
+ `INSERT INTO zip_county (zip, county_fips) VALUES ${vals.join(',')}
+ ON CONFLICT (zip) DO UPDATE SET county_fips = EXCLUDED.county_fips`,
+ params,
+ );
+ }
+ const c = await query<{ n: string }>(`SELECT count(*)::text AS n FROM zip_county`);
+ console.log(`[zip_county] loaded ${c.rows[0].n} rows`);
+ await pool.end();
+}
+main().catch(e => { console.error(e); process.exit(1); });
diff --git a/src/server/index.ts b/src/server/index.ts
index 64a9f49..c58f0bc 100644
--- a/src/server/index.ts
+++ b/src/server/index.ts
@@ -129,6 +129,38 @@ app.get('/api/geo-search', async (req, res) => {
try {
const q = String(req.query.q || '').trim();
if (q.length < 2) return res.json({ q, results: [] });
+
+ // ZIP → primary county (zip_county crosswalk). National.
+ if (/^\d{5}$/.test(q)) {
+ const r = await query<{ canonical_key: string; name: string; state_code: string | null }>(
+ `SELECT rg.canonical_key, rg.name, rg.state_code
+ FROM zip_county zc JOIN region rg ON rg.fips = zc.county_fips AND rg.region_type = 'county'
+ WHERE zc.zip = $1`, [q]);
+ if (r.rows.length) { const x = r.rows[0];
+ return res.json({ q, results: [{ type: 'zip', name: `${x.name} — ZIP ${q}`, state: x.state_code || null,
+ key: x.canonical_key, url: '/market.html?key=' + encodeURIComponent(x.canonical_key) }] }); }
+ return res.json({ q, results: [] });
+ }
+
+ // street address (starts with a house number) → Census geocoder → county. National.
+ if (/^\d+\s+\S/.test(q)) {
+ try {
+ const gu = 'https://geocoding.geo.census.gov/geocoder/geographies/onelineaddress?address='
+ + encodeURIComponent(q) + '&benchmark=Public_AR_Current&vintage=Current_Current&format=json';
+ const gj: any = await (await fetch(gu, { signal: AbortSignal.timeout(6000) })).json();
+ const m = gj?.result?.addressMatches?.[0];
+ const co = m?.geographies?.Counties?.[0];
+ if (co?.GEOID) {
+ const r = await query<{ canonical_key: string; name: string; state_code: string | null }>(
+ `SELECT canonical_key, name, state_code FROM region WHERE fips = $1 AND region_type = 'county'`, [co.GEOID]);
+ if (r.rows.length) { const x = r.rows[0];
+ return res.json({ q, results: [{ type: 'address', name: m.matchedAddress || q, state: x.state_code || null,
+ sub: x.name, key: x.canonical_key, url: '/market.html?key=' + encodeURIComponent(x.canonical_key) }] }); }
+ }
+ } catch (e) { /* geocoder timeout/failure → fall through to name search */ }
+ return res.json({ q, results: [] });
+ }
+
const esc = q.replace(/[%_\\]/g, '\\$&');
const like = '%' + esc + '%';
const starts = esc + '%';
← 0a3f34d USRE: top location search (county/city/state) — /api/geo-sea
·
back to Nationalrealestate
·
USRE: searched address in a parcel-covered county deep-links 7c0710f →