← back to Nationalrealestate
feat: Parcel Explorer national coverage overview map + lint/refactor, v0.17.0 (session close)
956d71906d7e18b9e80780f562aa5ef658494393 · 2026-08-10 21:17:02 -0700 · Steve Abrams
- overview map: /api/parcels/coverage now returns per-county AVG centroid (clat/clng);
parcels.html plots a marker per covered county (sized by count, gold-ringed=priced),
click to drill in + '‹ All counties' back button.
- security hardening (lint): safeHref() strips javascript:/data: URIs on DB-sourced
hrefs; client-side validFips()/sid guards on deep-links.
- refactor: renamed shadowed 'map' var in headerSort; split jammed const decl.
tsc clean, JS syntax clean, verified render (32 counties mapped). Local pm2 restarted.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Files touched
M package.jsonM public/parcels.htmlM src/server/parcels.ts
Diff
commit 956d71906d7e18b9e80780f562aa5ef658494393
Author: Steve Abrams <steve@designerwallcoverings.com>
Date: Mon Aug 10 21:17:02 2026 -0700
feat: Parcel Explorer national coverage overview map + lint/refactor, v0.17.0 (session close)
- overview map: /api/parcels/coverage now returns per-county AVG centroid (clat/clng);
parcels.html plots a marker per covered county (sized by count, gold-ringed=priced),
click to drill in + '‹ All counties' back button.
- security hardening (lint): safeHref() strips javascript:/data: URIs on DB-sourced
hrefs; client-side validFips()/sid guards on deep-links.
- refactor: renamed shadowed 'map' var in headerSort; split jammed const decl.
tsc clean, JS syntax clean, verified render (32 counties mapped). Local pm2 restarted.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---
package.json | 2 +-
public/parcels.html | 39 +++++++++++++++++++++++++++++++++------
src/server/parcels.ts | 6 +++++-
3 files changed, 39 insertions(+), 8 deletions(-)
diff --git a/package.json b/package.json
index 7f02df9..f12099c 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "nationalrealestate",
- "version": "0.16.1",
+ "version": "0.17.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.",
diff --git a/public/parcels.html b/public/parcels.html
index 42c9fd3..e55f035 100644
--- a/public/parcels.html
+++ b/public/parcels.html
@@ -104,6 +104,7 @@
<div id="map"></div>
<div id="tablewrap"><div class="empty">Pick a county on the left to explore its parcels.</div></div>
<div id="status">
+ <button id="backbtn" style="display:none">‹ All counties</button>
<span id="count">—</span>
<div id="colmenu"><button id="colbtn">Columns ▾</button><div id="colpop"></div></div>
<button id="prev" disabled>‹ Prev</button><button id="next" disabled>Next ›</button>
@@ -121,6 +122,8 @@ const $ = s => document.querySelector(s);
const fmt = n => n==null?'—':Number(n).toLocaleString();
const money = n => n==null?'—':'$'+Number(n).toLocaleString(undefined,{maximumFractionDigits:0});
const esc = s => (s==null?'':String(s)).replace(/[&<>"]/g,c=>({'&':'&','<':'<','>':'>','"':'"'}[c]));
+const safeHref = u => { try{ const p=new URL(String(u||'')); return (p.protocol==='http:'||p.protocol==='https:')?esc(p.href):'#'; }catch(e){ return '#'; } };
+const validFips = f => { const t=String(f||'').trim(); return /^\d{5}$/.test(t)?t:null; };
// columns: key, label, render, numeric, defaultOn
const COLS = [
@@ -177,6 +180,26 @@ async function loadMap(){
$('#mapnote').textContent = d.points.length?`${fmt(d.points.length)} plotted${d.capped?' (capped)':''}`:'no coordinates';
if(pts.length){ try{ map.fitBounds(L.latLngBounds(pts).pad(.05),{maxZoom:13}); }catch(e){} }
}
+// national coverage overview — one marker per covered county (sized by count, gold-ringed if priced)
+function showOverview(){
+ ptLayer.clearLayers(); state.county=null; state.selSid=null;
+ document.querySelectorAll('.cty.sel').forEach(x=>x.classList.remove('sel'));
+ $('#tablewrap').innerHTML='<div class="empty">Pick a county — on the map above or in the list — to explore its parcels.</div>';
+ $('#count').textContent='—'; $('#prev').disabled=$('#next').disabled=true; $('#backbtn').style.display='none';
+ const cs=(state.counties||[]).filter(c=>c.clat!=null&&c.clng!=null);
+ const pts=[];
+ const maxP=Math.max(1,...cs.map(c=>c.parcels));
+ for(const c of cs){
+ const rad=5+18*Math.sqrt(c.parcels/maxP);
+ const m=L.circleMarker([c.clat,c.clng],{radius:rad,weight:c.priced>0?2:1,
+ color:c.priced>0?'#c8a24b':'#3a4658',fillColor:c.priced>0?'#c8a24b':'#5a9be0',fillOpacity:.45});
+ m.on('click',()=>selectCounty(c.fips));
+ m.bindTooltip(`<span class="nm">${esc(c.name)}</span> · <span class="vl">${fmt(c.parcels)}</span>${c.priced>0?' ◆ '+fmt(c.priced)+' priced':''}`,{className:'county-tip'});
+ m.addTo(ptLayer); pts.push([c.clat,c.clng]);
+ }
+ $('#mapnote').textContent=`${cs.length} counties mapped · click one`;
+ if(pts.length){ try{ map.fitBounds(L.latLngBounds(pts).pad(.12),{maxZoom:7}); }catch(e){} }
+}
// ── sidebar coverage ──
async function loadCoverage(){
@@ -194,12 +217,15 @@ async function loadCoverage(){
el.onclick=()=>selectCounty(c.fips,el);
box.appendChild(el);
}
+ state.counties=d.counties||[];
+ if(!state.county) showOverview();
}
function selectCounty(fips,el){
document.querySelectorAll('.cty.sel').forEach(x=>x.classList.remove('sel'));
(el||document.querySelector(`.cty[data-fips="${fips}"]`))?.classList.add('sel');
- state.county=fips; state.offset=0; loadTable(); loadMap();
+ state.county=fips; state.offset=0; $('#backbtn').style.display=''; loadTable(); loadMap();
}
+$('#backbtn').onclick=()=>{ showOverview(); };
// ── table ──
function renderCols(){
@@ -241,8 +267,8 @@ function draw(){
if(window.ColResize) ColResize.refresh();
}
function headerSort(k){
- const map={total_value:'value_desc',last_sale_price:'price_desc',last_sale_date:'recent',address:'address',source_id:'',added:'added'};
- if(k in map){ state.sort=map[k]; $('#sort').value=map[k]; state.offset=0; loadTable(); }
+ const sortKeys={total_value:'value_desc',last_sale_price:'price_desc',last_sale_date:'recent',address:'address',source_id:'',added:'added'};
+ if(k in sortKeys){ state.sort=sortKeys[k]; $('#sort').value=sortKeys[k]; state.offset=0; loadTable(); }
}
// ── detail drawer ──
@@ -277,14 +303,14 @@ async function openDetail(fips,sid){
(det.grantor?'<div class="row"><span>Grantor</span>'+esc(det.grantor)+'</div>':'')+
(det.grantee?'<div class="row"><span>Grantee</span>'+esc(det.grantee)+'</div>':'')+
(e.doc_number?'<div class="row"><span>Doc #</span>'+esc(e.doc_number)+'</div>':'')+
- (e.source_url?'<div class="row"><span>Source</span><a href="'+esc(e.source_url)+'" target="_blank" rel="noopener">record ↗</a></div>':'')+
+ (e.source_url?'<div class="row"><span>Source</span><a href="'+safeHref(e.source_url)+'" target="_blank" rel="noopener">record ↗</a></div>':'')+
'</div>';
}
} else {
deeds='<div class="badge">Coverage-only — no recorded sale price (CA AB-1785)</div>';
}
let links='';
- if(d.links&&d.links.length){ links='<div style="margin-top:12px">'+d.links.map(l=>'<a href="'+esc(l.url)+'" target="_blank" rel="noopener">'+esc(l.label||l.kind)+' ↗</a>').join(' · ')+'</div>'; }
+ if(d.links&&d.links.length){ links='<div style="margin-top:12px">'+d.links.map(l=>'<a href="'+safeHref(l.url)+'" target="_blank" rel="noopener">'+esc(l.label||l.kind)+' ↗</a>').join(' · ')+'</div>'; }
$('#detailBody').innerHTML=`<h2>${esc(p.address||p.source_id)}</h2><div class="sub">${esc(p.county_name||p.county_fips)} ${esc(p.state_code||'')} · ${esc(p.source_id)}</div>${kv}${deeds}${links}`;
}
function closeDetail(){ $('#detail').classList.remove('open'); state.selSid=null; document.querySelectorAll('tbody tr.sel').forEach(t=>t.classList.remove('sel')); }
@@ -307,7 +333,8 @@ document.addEventListener('keydown',e=>{ if(e.key==='Escape') closeDetail(); });
initMap(); renderCols(); loadCoverage();
// deep-link ?county=06059&sid=...
const p=new URLSearchParams(location.search);
- if(p.get('county')){ state.county=p.get('county'); loadCoverage().then(()=>selectCounty(state.county)); if(p.get('sid')) setTimeout(()=>openDetail(state.county,p.get('sid')),600); }
+ const dlCounty=validFips(p.get('county'));
+ if(dlCounty){ state.county=dlCounty; const sid=p.get('sid')?String(p.get('sid')).trim().slice(0,64):null; loadCoverage().then(()=>selectCounty(dlCounty)); if(sid) setTimeout(()=>openDetail(dlCounty,sid),600); }
})();
</script>
</body>
diff --git a/src/server/parcels.ts b/src/server/parcels.ts
index 3fb9670..fb217db 100644
--- a/src/server/parcels.ts
+++ b/src/server/parcels.ts
@@ -32,6 +32,8 @@ export function mountParcels(app: Express) {
COUNT(p.last_sale_price)::bigint AS priced,
COUNT(p.lat)::bigint AS mapped,
COUNT(p.total_value)::bigint AS valued,
+ AVG(p.lat) FILTER (WHERE p.lat IS NOT NULL) AS clat,
+ AVG(p.lng) FILTER (WHERE p.lng IS NOT NULL) AS clng,
r.name, r.state_code
FROM parcel p
LEFT JOIN region r ON r.fips = p.county_fips AND r.region_type = 'county'
@@ -40,6 +42,7 @@ export function mountParcels(app: Express) {
const counties = r.rows.map((x) => ({
fips: x.county_fips, name: x.name || x.county_fips, state: x.state_code || null,
parcels: Number(x.parcels), priced: Number(x.priced), mapped: Number(x.mapped), valued: Number(x.valued),
+ clat: x.clat != null ? +(+x.clat).toFixed(4) : null, clng: x.clng != null ? +(+x.clng).toFixed(4) : null,
}));
const totals = counties.reduce((a, c) => ({ parcels: a.parcels + c.parcels, priced: a.priced + c.priced, counties: a.counties + 1 }), { parcels: 0, priced: 0, counties: 0 });
coverageCache = { at: Date.now(), rows: counties, totals };
@@ -57,7 +60,8 @@ export function mountParcels(app: Express) {
const q = String(req.query.q || '').trim().slice(0, 80);
const pricedOnly = req.query.priced === '1';
- const where: string[] = ['p.county_fips = $1']; const params: any[] = [cf];
+ const where: string[] = ['p.county_fips = $1'];
+ const params: any[] = [cf];
if (pricedOnly) where.push('p.last_sale_price IS NOT NULL');
if (q) {
const esc = q.replace(/[%_\\]/g, '\\$&');
← d6f64ff TK-16: add Parcel Explorer web viewer (/parcels.html + /api/
·
back to Nationalrealestate
·
auto-data-snapshot: 2026-08-10T21:18:09 (1 data files) — pac cd7be34 →