← back to Nationalrealestate
public/property.html
365 lines
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="icon" href="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3E%3Crect width='16' height='16' rx='3' fill='%230a0d13'/%3E%3Ctext x='8' y='12' font-size='10' text-anchor='middle' fill='%23c8a24b'%3EUS%3C/text%3E%3C/svg%3E">
<title>USRealEstate — Property Detail</title>
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css"
integrity="sha256-p4NxAoJBhIIN+hmNHrzRCf9tD/miZyoHS5obTRR9BMY=" crossorigin="">
<style>
:root{--bg:#0a0d13;--panel:#11151d;--line:#222936;--fg:#e8ecf2;--muted:#8a93a3;--gold:#c8a24b;
--green:#5fae7c;--red:#c96a5a;}
*{box-sizing:border-box;}
html,body{margin:0;background:var(--bg);color:var(--fg);
font:14px/1.5 -apple-system,Segoe UI,Roboto,sans-serif;}
#topbar{position:sticky;top:0;z-index:1000;display:flex;align-items:center;gap:18px;
padding:10px 18px;background:rgba(17,21,29,.94);border-bottom:1px solid var(--line);backdrop-filter:blur(6px);}
#topbar .brand{font-size:16px;font-weight:700;white-space:nowrap;}
#topbar .brand b{color:var(--gold);}
nav{display:flex;gap:14px;font-size:13px;}
nav a{color:var(--muted);text-decoration:none;} nav a:hover,nav a.active{color:var(--gold);}
main{max-width:1180px;margin:0 auto;padding:24px 18px 60px;}
#search{display:flex;gap:10px;margin-bottom:14px;}
#search input{flex:1;max-width:520px;background:var(--panel);border:1px solid var(--line);
border-radius:10px;color:var(--fg);padding:10px 14px;font-size:14px;outline:none;}
#search input:focus{border-color:var(--gold);}
#search button{background:var(--gold);color:#0a0d13;border:0;border-radius:10px;padding:10px 18px;
font-weight:700;cursor:pointer;font-size:14px;}
#results{margin-bottom:18px;}
.result{display:flex;gap:14px;align-items:baseline;padding:8px 12px;border:1px solid var(--line);
border-radius:8px;margin-bottom:6px;cursor:pointer;background:var(--panel);flex-wrap:wrap;}
.result:hover{border-color:var(--gold);}
.result .addr{font-weight:700;}
.result .facts{color:var(--muted);font-size:12px;font-variant-numeric:tabular-nums;}
#hdr{display:flex;align-items:baseline;gap:18px;flex-wrap:wrap;margin-bottom:6px;}
#hdr h1{margin:0;font-size:24px;font-weight:700;}
#hdr .st{color:var(--muted);font-size:14px;}
#hdr .val{font-size:20px;color:var(--gold);font-variant-numeric:tabular-nums;font-weight:700;}
#sub{color:var(--muted);font-size:12px;margin-bottom:18px;}
.grid2{display:grid;grid-template-columns:repeat(auto-fit,minmax(440px,1fr));gap:16px;}
.panel{background:var(--panel);border:1px solid var(--line);border-radius:12px;padding:14px 16px;}
.panel h3{margin:0 0 8px;font-size:13px;color:var(--gold);font-weight:700;text-transform:uppercase;
letter-spacing:.5px;}
.pending{color:var(--muted);font-size:12px;padding:14px 0;font-style:italic;}
.kv{display:grid;grid-template-columns:repeat(auto-fill,minmax(150px,1fr));gap:10px;}
.stat{background:var(--bg);border:1px solid var(--line);border-radius:10px;padding:10px 12px;}
.stat .k{font-size:11px;color:var(--muted);text-transform:uppercase;letter-spacing:.4px;}
.stat .v{font-size:16px;font-weight:700;font-variant-numeric:tabular-nums;margin-top:2px;}
table{width:100%;border-collapse:collapse;font-size:12px;font-variant-numeric:tabular-nums;}
th{color:var(--muted);text-align:left;font-weight:600;padding:5px 8px;border-bottom:1px solid var(--line);
text-transform:uppercase;font-size:10px;letter-spacing:.4px;}
td{padding:6px 8px;border-bottom:1px solid var(--line);}
tr:last-child td{border-bottom:0;}
td a{color:var(--gold);text-decoration:none;}
.note{color:var(--muted);font-size:11px;margin-top:8px;font-style:italic;}
.badge{display:inline-block;padding:3px 10px;border-radius:14px;font-size:12px;font-weight:700;
border:1px solid var(--line);margin-right:6px;}
.badge.warn{color:var(--red);border-color:var(--red);}
.badge.ok{color:var(--green);border-color:var(--green);}
#map_el{height:260px;border-radius:8px;}
.leaflet-container{background:var(--bg);}
#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">
<div class="brand"><b>USRealEstate</b> — Property Detail</div>
<nav>
<a href="/">Map</a>
<a href="/markets.html">Markets</a><a href="/commercial.html">Commercial</a>
<a href="/watchlist.html">Watchlist</a>
<a href="/brokers.html">Brokers</a>
<a href="/sources.html">Sources</a>
<a href="/admin.html">Admin</a>
</nav>
</div>
<main>
<form id="search" style="display:flex;gap:8px;flex-wrap:wrap;align-items:center;">
<select id="countysel" style="background:var(--bg,#0a0d13);border:1px solid var(--line,#222936);color:var(--fg,#e8ecf2);border-radius:8px;padding:8px 10px;font-size:13px;"></select>
<input id="q" type="text" placeholder="Search an address" autocomplete="off" style="flex:1;min-width:240px;">
<button type="submit">Search</button>
</form>
<div id="results"></div>
<div id="prop">
<div id="hdr"><h1></h1></div>
<div id="sub"></div>
<div class="grid2">
<div class="panel" id="s_history"><h3>Property History</h3><div class="body"></div></div>
<div class="panel" id="s_tax"><h3>Tax History</h3><div class="body"></div></div>
<div class="panel" id="s_contact"><h3>Contact Information</h3><div class="body"></div></div>
<div class="panel" id="s_ownership"><h3>Ownership Information</h3><div class="body"></div></div>
<div class="panel" id="s_zoning"><h3>Zoning</h3><div class="body"></div></div>
<div class="panel" id="s_details"><h3>Property Details</h3><div class="body"></div></div>
<div class="panel" id="s_contacts"><h3>Contacts</h3><div class="body"></div></div>
<div class="panel" id="s_map"><h3>Map</h3><div class="body"></div></div>
</div>
<div class="panel" style="margin-top:16px;" id="s_comps"><h3>Comparable Properties</h3><div class="body"></div></div>
<div class="grid2" style="margin-top:16px;">
<div class="panel" id="s_climate"><h3>Climate Risk</h3><div class="body"></div></div>
<div class="panel" id="s_rental"><h3>Rental Market Overview</h3><div class="body"></div></div>
<div class="panel" id="s_demo"><h3>Demographics</h3><div class="body"></div></div>
<div class="panel" id="s_location"><h3>Location Insights</h3><div class="body"></div></div>
</div>
<div id="foot">Parcel facts: LA County assessor roll (via CRCP) · NYC MapPLUTO (owner + zoning + assessed) ·
King County WA EXTR (deed sale history + appraised values) · Cook County IL Assessor (owner + assessed).
Climate: FEMA National Risk Index v1.20.0. Rents: HUD FMR FY2026 + Zillow ZORI. Demographics: Census ACS 2023 5-yr. $0 — all free public data.</div>
</div>
</main>
<script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"
integrity="sha256-20nQCchB9co0qIjJZRGuk2/Z9VM+kNiyxNV1lvTlZBo=" crossorigin=""></script>
<script>
const qs = new URLSearchParams(location.search);
let county = qs.get('county') || '06037';
const COUNTY_LABELS = {
'06037': 'Los Angeles County, CA', '36061': 'Manhattan (New York County), NY',
'36047': 'Brooklyn (Kings County), NY', '36081': 'Queens County, NY',
'36005': 'Bronx County, NY', '36085': 'Staten Island (Richmond County), NY',
'53033': 'King County (Seattle), WA', '17031': 'Cook County (Chicago), IL',
};
const PLACEHOLDER = { '06037': 'e.g. 9321 Ventura Way', '53033': 'e.g. 400 Broad St',
'17031': 'e.g. 1412 N Kingsbury St', dflt: 'e.g. 1 5 Avenue' };
const usd0 = v => v == null ? '—' : '$' + Math.round(v).toLocaleString();
const int0 = v => v == null ? '—' : Math.round(v).toLocaleString();
const pct1 = v => v == null ? '—' : (v * 100).toFixed(1) + '%';
const num1 = v => v == null ? '—' : (+v).toFixed(1);
const esc = s => String(s ?? '').replace(/[&<>"']/g, c => ({'&':'&','<':'<','>':'>','"':'"',"'":'''}[c]));
const PENDING = '<div class="pending">Coverage pending for this county.</div>';
function pendingNote(note){ return '<div class="pending">' + esc(note || 'Coverage pending for this county.') + '</div>'; }
function stats(pairs){
return '<div class="kv">' + pairs.map(([k, v]) =>
`<div class="stat"><div class="k">${esc(k)}</div><div class="v">${v}</div></div>`).join('') + '</div>';
}
function body(id, html){ document.querySelector('#' + id + ' .body').innerHTML = html; }
let mapObj = null;
async function search(q){
const el = document.getElementById('results');
el.innerHTML = '<div class="pending">Searching…</div>';
const res = await fetch(`/api/property/search?county=${county}&q=${encodeURIComponent(q)}`);
const d = await res.json();
if (!res.ok){ el.innerHTML = pendingNote(d.error); return; }
if (!d.matches.length){ el.innerHTML = '<div class="pending">No parcels matched. Try "number street", e.g. 9321 Ventura.</div>'; return; }
el.innerHTML = d.matches.map(m =>
`<div class="result" data-ain="${esc(m.ain)}">
<span class="addr">${esc(m.address)}</span>
<span class="facts">${esc(m.use || '')} · ${m.bedrooms ?? '—'}bd/${m.bathrooms ?? '—'}ba ·
${int0(m.sqft)} sqft · built ${m.year_built || '—'} · assessed ${usd0(m.assessed_value)}</span>
</div>`).join('');
el.querySelectorAll('.result').forEach(r => r.addEventListener('click', () => {
history.replaceState(null, '', `?county=${county}&loc=${encodeURIComponent(r.dataset.ain)}`);
load(r.dataset.ain);
el.innerHTML = '';
}));
}
async function load(loc){
const res = await fetch(`/api/property?county=${county}&loc=${encodeURIComponent(loc)}`);
const d = await res.json();
if (!res.ok){
document.querySelector('#hdr h1').textContent = 'Not found: ' + loc;
return;
}
const p = d.property_details;
document.title = 'USRealEstate — ' + (p.address || p.ain);
document.querySelector('#hdr h1').textContent = p.address || 'AIN ' + p.ain;
document.querySelector('#hdr').innerHTML =
`<h1>${esc(p.address || 'AIN ' + p.ain)}</h1><span class="st">${esc(p.use || '')}</span>` +
`<span class="val">${usd0(d.tax_history.rolls[0]?.total_value)} assessed</span>`;
document.getElementById('sub').textContent =
`AIN ${p.ain} · county ${d.county} · ${p.bedrooms ?? '—'} bd / ${p.bathrooms ?? '—'} ba · ` +
`${int0(p.sqft)} sqft · built ${p.year_built || '—'}`;
// 1 Property History — recorded-document events, sale records, and public-record links
const sales = d.property_history.sales || [];
const events = d.property_history.events || [];
const pr = d.public_records || [];
const recLinks = pr.length
? `<div class="note"><b>Public records:</b> ` + pr.map(r =>
`<a href="${esc(r.url)}" target="_blank" rel="noopener noreferrer">${esc(r.label)}</a>${r.deep_link ? ' ↗' : ''}`).join(' · ')
+ (pr.some(r => r.deep_link === false) ? `<br><span style="opacity:.7">↗ = direct record; others are search portals (enter the APN)</span>` : '') + `</div>`
: '';
const histCore = events.length
? `<table><tr><th>Date</th><th>Type</th><th>Doc #</th><th>Record</th></tr>` +
events.map(e => `<tr><td>${esc(e.date)}</td><td>${esc(e.event_type)}</td><td>${esc(e.doc_number || '—')}</td>
<td>${e.source_url ? `<a href="${esc(e.source_url)}" target="_blank" rel="noopener noreferrer">view</a>` : '—'}</td></tr>`).join('') +
`</table><div class="note">${esc(d.property_history.note)}</div>`
: sales.length
? `<table><tr><th>Date</th><th>Price</th><th>Buyer</th><th>Seller</th></tr>` +
sales.map(s => `<tr><td>${esc(s.date)}</td><td>${usd0(s.price)}</td>
<td>${esc(s.buyer || '—')}</td><td>${esc(s.seller || '—')}</td></tr>`).join('') +
`</table><div class="note">${esc(d.property_history.note)}</div>`
: d.property_history.last_recording_date
? stats([['Last Recording Date', esc(d.property_history.last_recording_date)],
['Sale Price', usd0(d.property_history.sale_price)]]) +
`<div class="note">${esc(d.property_history.note)}</div>`
: pendingNote(d.property_history.note);
body('s_history', histCore + recLinks);
// 2 Tax History
const rolls = d.tax_history.rolls.filter(r => r.roll_year);
body('s_tax', rolls.length
? `<table><tr><th>Roll Year</th><th>Total</th><th>Land</th><th>Improvements</th></tr>` +
rolls.map(r => `<tr><td>${esc(r.roll_year)}</td><td>${usd0(r.total_value)}</td>
<td>${usd0(r.land_value)}</td><td>${usd0(r.improvement_value)}</td></tr>`).join('') +
`</table><div class="note">${esc(d.tax_history.note)}</div>`
: pendingNote(d.tax_history.note));
// 3 Contact Information
body('s_contact', (d.contact_information.phone || d.contact_information.email)
? stats([['Phone', esc(d.contact_information.phone)], ['Email', esc(d.contact_information.email)]])
: pendingNote(d.contact_information.note));
// 4 Ownership Information
body('s_ownership', d.ownership_information.owner_name
? stats([['Owner', esc(d.ownership_information.owner_name)],
['Mailing Address', esc(d.ownership_information.mailing_address)]])
: pendingNote(d.ownership_information.note));
// 5 Zoning
body('s_zoning', d.zoning.code
? stats([['Zoning', esc(d.zoning.code)]])
: pendingNote(d.zoning.note));
// 6 Property Details
body('s_details', stats([
['Use', esc(p.use)], ['Category', esc(p.use_category)],
['Year Built', p.year_built || '—'], ['Effective Year', p.effective_year_built || '—'],
['Sqft (main)', int0(p.sqft)], ['Bedrooms', p.bedrooms ?? '—'],
['Bathrooms', p.bathrooms ?? '—'], ['Units', p.units ?? '—'],
]));
// 7 Contacts
const ca = d.contacts.county_assessor;
body('s_contacts',
stats([['County Assessor', esc(ca.name)], ['Phone', esc(ca.phone)]]) +
`<div class="note"><a href="${esc(ca.website)}" target="_blank" rel="noopener noreferrer" style="color:var(--gold)">${esc(ca.website)}</a>
· <a href="${esc(d.contacts.brokers_link)}" style="color:var(--gold)">CA brokers directory</a></div>` +
`<div class="note">${esc(d.contacts.note)}</div>`);
// 8 Map
if (d.map.lat != null && d.map.lng != null){
body('s_map', `<div id="map_el"></div><div class="note">${esc(d.map.note)}</div>`);
if (mapObj) mapObj.remove();
mapObj = L.map('map_el', { scrollWheelZoom: false }).setView([d.map.lat, d.map.lng], d.map.precision === 'parcel' ? 17 : 9);
L.tileLayer('https://{s}.basemaps.cartocdn.com/dark_all/{z}/{x}/{y}{r}.png',
{ attribution: '© OpenStreetMap © CARTO', maxZoom: 19 }).addTo(mapObj);
L.marker([d.map.lat, d.map.lng]).addTo(mapObj)
.bindPopup(esc(p.address || p.ain) + '<br><small>' + esc(d.map.precision) + '</small>');
} else {
body('s_map', pendingNote(d.map.note));
}
// 9 Comparable Properties
body('s_comps', d.comps.length
? `<table><tr><th>Address</th><th>Built</th><th>Sqft</th><th>Bd/Ba</th><th>Assessed</th><th>Recorded</th></tr>` +
d.comps.map(c => `<tr><td><a href="?county=${county}&loc=${encodeURIComponent(c.ain)}">${esc(c.address)}</a></td>
<td>${c.year_built || '—'}</td><td>${int0(c.sqft)}</td>
<td>${c.bedrooms ?? '—'}/${c.bathrooms ?? '—'}</td>
<td>${usd0(c.assessed_value)}</td><td>${esc(c.recording_date || '—')}</td></tr>`).join('') +
`</table><div class="note">Same use type, sqft ±30%, most recent recordings; same-street matches first.</div>`
: pendingNote('No comparable parcels matched (needs sqft + use type on the subject parcel).'));
// 10 Climate Risk
if (d.climate && d.climate.risk_score != null){
const rt = d.climate.risk_rating || '—';
const cls = /high/i.test(rt) ? 'warn' : 'ok';
body('s_climate',
`<div style="margin-bottom:10px;"><span class="badge ${cls}">${esc(rt)}</span>
${(d.climate.top_hazards || []).map(h => `<span class="badge">${esc(h.hazard)} ${num1(h.score)}</span>`).join('')}</div>` +
stats([
['Risk Score', num1(d.climate.risk_score)], ['Expected Annual Loss', num1(d.climate.eal_score)],
['Social Vulnerability', num1(d.climate.social_vulnerability)],
['Community Resilience', num1(d.climate.community_resilience)],
]) + `<div class="note">${esc(d.climate.source)}</div>`);
} else body('s_climate', PENDING);
// 11 Rental Market Overview
if (d.rental && (d.rental.fmr || d.rental.zori != null)){
const f = d.rental.fmr || {};
body('s_rental', stats([
['FMR Studio', usd0(f.fmr_0br)], ['FMR 1BR', usd0(f.fmr_1br)], ['FMR 2BR', usd0(f.fmr_2br)],
['FMR 3BR', usd0(f.fmr_3br)], ['FMR 4BR', usd0(f.fmr_4br)],
['ZORI (observed rent)', usd0(d.rental.zori)], ['Rent Yield', pct1(d.rental.rent_yield)],
['Median Gross Rent', usd0(d.rental.median_gross_rent)],
]) + `<div class="note">${esc(d.rental.source)}</div>`);
} else body('s_rental', PENDING);
// 12 Demographics
if (d.demographics && d.demographics.population != null){
body('s_demo', stats([
['Population', int0(d.demographics.population)],
['Median HH Income', usd0(d.demographics.median_hh_income)],
['Median Gross Rent', usd0(d.demographics.median_gross_rent)],
['Vacancy Rate', pct1(d.demographics.vacancy_rate)],
['Renter Share', pct1(d.demographics.renter_share)],
]) + `<div class="note">${esc(d.demographics.source)}</div>`);
} else body('s_demo', PENDING);
// 13 Location Insights
if (d.location){
body('s_location', stats([
['County', esc(d.location.county_name)], ['State', esc(d.location.state)],
['Metro', esc(d.location.metro || '—')], ['FIPS', esc(d.location.fips)],
['County ZHVI', usd0(d.location.zhvi)], ['ZHVI YoY', pct1(d.location.zhvi_yoy)],
]) + `<div class="note"><a href="${esc(d.location.links.market)}" style="color:var(--gold)">Full county market detail →</a></div>`);
} else body('s_location', PENDING);
}
document.getElementById('search').addEventListener('submit', e => {
e.preventDefault();
const q = document.getElementById('q').value.trim();
if (q) search(q);
});
// full 13-section skeleton always visible — never blank/hidden
document.querySelector('#hdr h1').textContent = 'Search an address to load a property';
document.querySelectorAll('#prop .panel .body').forEach(b => { b.innerHTML = PENDING; });
function applyCounty(){
document.getElementById('q').placeholder = 'Search an address — ' + (PLACEHOLDER[county] || PLACEHOLDER.dflt);
const lbl = COUNTY_LABELS[county] || county;
document.getElementById('sub').textContent =
'Parcel coverage: ' + lbl + '. Switch counties above; more counties added over time.';
}
async function initCounties(){
const sel = document.getElementById('countysel');
try {
const d = await fetch('/api/property/coverage').then(r => r.json());
const opts = (d.counties || []).map(c =>
`<option value="${c.county_fips}"${c.county_fips === county ? ' selected' : ''}>${COUNTY_LABELS[c.county_fips] || c.county_fips} · ${(+c.parcel_count).toLocaleString()} parcels</option>`).join('');
sel.innerHTML = opts || `<option value="06037">Los Angeles County, CA</option>`;
} catch { sel.innerHTML = `<option value="06037">Los Angeles County, CA</option>`; }
sel.onchange = () => {
county = sel.value;
document.getElementById('results').innerHTML = '';
document.querySelectorAll('#prop .panel .body').forEach(b => { b.innerHTML = PENDING; });
document.querySelector('#hdr h1').textContent = 'Search an address to load a property';
applyCounty();
history.replaceState(null, '', `?county=${county}`);
};
applyCounty();
}
initCounties();
const loc = qs.get('loc');
const prefillQ = qs.get('q');
if (loc) load(loc);
else if (prefillQ) { // deep-linked from the top location search — prefill + auto-run the address search
const qi = document.getElementById('q'); qi.value = prefillQ; search(prefillQ);
}
</script>
<script src="/nav-drawer.js"></script>
<script src="/geo-search.js"></script>
</body>
</html>