← back to Re Flyer Aggregator
property index: enrich with size — carry RENTV size_label/units/sqft/occupancy through ingest; extract units/sqft from titles (358-unit, 102,000 sf); show size + occupancy chips on /properties cards. Plumbing ready for usre parcel sqft to populate at scale (TK-10708)
1b6357c36c920acc96430b558e3229e754ac73df · 2026-08-20 09:12:51 -0700 · Steve Abrams
Files touched
M public/properties/index.htmlM scripts/build-property-index.mjsM scripts/ingest-rentv.mjs
Diff
commit 1b6357c36c920acc96430b558e3229e754ac73df
Author: Steve Abrams <steve@designerwallcoverings.com>
Date: Thu Aug 20 09:12:51 2026 -0700
property index: enrich with size — carry RENTV size_label/units/sqft/occupancy through ingest; extract units/sqft from titles (358-unit, 102,000 sf); show size + occupancy chips on /properties cards. Plumbing ready for usre parcel sqft to populate at scale (TK-10708)
---
public/properties/index.html | 2 +-
scripts/build-property-index.mjs | 10 ++++++++++
scripts/ingest-rentv.mjs | 2 +-
3 files changed, 12 insertions(+), 2 deletions(-)
diff --git a/public/properties/index.html b/public/properties/index.html
index 03bf758..4267b35 100644
--- a/public/properties/index.html
+++ b/public/properties/index.html
@@ -46,7 +46,7 @@ function render(){
a.sort(sort==='date'?(x,y)=>(Date.parse(y.latest)||0)-(Date.parse(x.latest)||0):sort==='price'?(x,y)=>pn(y.top_price)-pn(x.top_price):(x,y)=>y.mentions-x.mentions||(Date.parse(y.latest)||0)-(Date.parse(x.latest)||0));
document.getElementById('stat').textContent=`${a.length.toLocaleString()} properties${Q?` · “${Q}”`:''}`;
document.getElementById('rows').innerHTML=a.slice(0,600).map((p,i)=>{const k=p.address+'|'+(p.city||'');const open=EXP.has(k);
- return `<div class="p" data-k="${esc(k)}"><div class="top"><span class="addr">${open?'▾ ':'▸ '}${hl(p.address)}</span><span class="chip">${(p.property_type||'CRE').toUpperCase()}</span><span class="pr">${p.top_price||p.latest_price||''}</span><span class="loc">📍 ${esc([p.city,p.state].filter(Boolean).join(', '))}</span><span class="mn">${p.mentions}× · ${p.source_count} src</span></div>
+ return `<div class="p" data-k="${esc(k)}"><div class="top"><span class="addr">${open?'▾ ':'▸ '}${hl(p.address)}</span><span class="chip">${(p.property_type||'CRE').toUpperCase()}</span><span class="pr">${p.top_price||p.latest_price||''}</span><span class="loc">📍 ${esc([p.city,p.state].filter(Boolean).join(', '))}</span>${p.units?`<span class="mn" style="background:#e0f2fe;color:#075985">${p.units} units</span>`:''}${p.sqft?`<span class="mn" style="background:#e0f2fe;color:#075985">${(+p.sqft).toLocaleString()} sf</span>`:''}${p.occupancy_pct?`<span class="mn" style="background:#dcfce7;color:#166534">${p.occupancy_pct}% occ</span>`:''}<span class="mn">${p.mentions}× · ${p.source_count} src</span></div>
<div class="sub">latest: ${esc(p.latest_txn||'')} ${dt(p.latest)}</div>${open?hist(p):''}</div>`;}).join('')||'<div style="padding:24px;color:#9aa">No properties.</div>';
}
document.getElementById('rows').addEventListener('click',e=>{ if(e.target.closest('a'))return; const row=e.target.closest('.p'); if(!row)return; const k=row.dataset.k; EXP.has(k)?EXP.delete(k):EXP.add(k); render(); });
diff --git a/scripts/build-property-index.mjs b/scripts/build-property-index.mjs
index e17e0f1..cb4c0de 100644
--- a/scripts/build-property-index.mjs
+++ b/scripts/build-property-index.mjs
@@ -17,6 +17,14 @@ const arch = load(join(ROOT, 'data', 'article-archive.json'));
const ADDR = /\b(\d{2,5}(?:-\d{2,5})?\s+(?:[NSEW]\.?\s+)?[A-Z][a-zA-Z]+(?:\s+[A-Z][a-zA-Z]+){0,3}\s+(?:Ave|Avenue|St|Street|Blvd|Boulevard|Dr|Drive|Rd|Road|Way|Ln|Lane|Ct|Court|Pl|Place|Pkwy|Parkway|Hwy|Highway|Cir|Circle|Ter|Terrace|Sq|Square))\b/;
const PTYPE = /\b(office|industrial|warehouse|logistics|retail|multifamily|apartment|residential|hotel|resort|hospitality|medical|life science|lab|self.?storage|storage|mixed.?use|data center|land|shopping center|mall|flex)\b/i;
+const SQFT = /([\d,]{2,})\s*(?:sq\.?\s?ft|sf|square[- ]?f(?:ee|oo)t)\b/i;
+const UNITS = /([\d,]{1,6})[- ]unit\b/i;
+const sizeOf = r => {
+ const t = r.title || '';
+ const units = r.units || ((UNITS.exec(t) || [])[1] || '').replace(/,/g, '') || null;
+ const sqft = r.sqft || ((SQFT.exec(t) || [])[1] || '').replace(/,/g, '') || null;
+ return { units: units ? +units : null, sqft: sqft ? +sqft : null, size_label: r.size_label || null, occupancy_pct: r.occupancy_pct || null };
+};
const canon = a => a.toLowerCase().replace(/\b(ave|avenue|st|street|blvd|boulevard|dr|drive|rd|road|way|ln|lane|ct|court|pl|place|pkwy|parkway|hwy|highway|cir|circle|ter|terrace|sq|square)\b/g, m => m[0]).replace(/[^a-z0-9]+/g, ' ').trim();
const num = x => typeof x.price === 'number' ? x.price : null;
const cityState = m => { const p = (m || '').split(',').map(s => s.trim()); return { city: p[0] || null, state: p[1] || null }; };
@@ -34,6 +42,7 @@ for (const r of [...store, ...arch]) {
const p = props.get(key);
if (!p.property_type) p.property_type = r.property_type || (PTYPE.exec(r.title || '') || [])[1] || null;
if (city && !p.city) p.city = city; if (state && !p.state) p.state = state;
+ const sz = sizeOf(r); if (!p.units && sz.units) p.units = sz.units; if (!p.sqft && sz.sqft) p.sqft = sz.sqft; if (!p.size_label && sz.size_label) p.size_label = sz.size_label; if (!p.occupancy_pct && sz.occupancy_pct) p.occupancy_pct = sz.occupancy_pct;
p.sources.add(r.source);
p.entries.push({ date: r.date, txn: r.type, price: num(r), price_label: r.price_label || r.label || (typeof r.price === 'string' ? r.price : null), source: r.source, link: r.link, title: r.title, buyer: r.buyer || null, seller: r.seller || null });
}
@@ -46,6 +55,7 @@ const out = [...props.values()].map(p => {
const priced = ent.filter(x => x.price);
return {
address: p.address, city: p.city, state: p.state, property_type: p.property_type,
+ units: p.units || null, sqft: p.sqft || null, size_label: p.size_label || null, occupancy_pct: p.occupancy_pct || null,
latest: ent[0]?.date, latest_txn: ent[0]?.txn, latest_price: ent[0]?.price_label,
top_price: priced.sort((a, b) => (b.price || 0) - (a.price || 0))[0]?.price_label || null,
mentions: ent.length, source_count: p.sources.size, sources: [...p.sources], entries: ent
diff --git a/scripts/ingest-rentv.mjs b/scripts/ingest-rentv.mjs
index 66127f0..174f713 100644
--- a/scripts/ingest-rentv.mjs
+++ b/scripts/ingest-rentv.mjs
@@ -22,7 +22,7 @@ const news = arr(await get('/api/news'));
const recs = [];
for (const d of deals) {
const mkt = [d.city, d.state].filter(Boolean).join(', ') || d.state || 'CA';
- recs.push({ type: d.txn_type || 'Sale', price: d.amount || null, label: d.amount_label || null, market: mkt, title: d.title, source: 'RENTV', link: abs(d.url), date: now, property_type: d.property_type || null, address: d.address || null, buyer: null, seller: null });
+ recs.push({ type: d.txn_type || 'Sale', price: d.amount || null, label: d.amount_label || null, market: mkt, title: d.title, source: 'RENTV', link: abs(d.url), date: now, property_type: d.property_type || null, address: d.address || null, size_label: d.size_label || null, units: d.units || null, sqft: d.sqft || null, occupancy_pct: d.occupancy_pct || null, buyer: null, seller: null });
}
for (const n of news) recs.push({ type: 'News', price: null, label: null, market: n.cat || 'CA', title: n.title, source: 'RENTV', link: abs(n.url), date: now, buyer: null, seller: null });
← 8d7215d scale-up per-property index to FULL universe: ingest-usre-pa
·
back to Re Flyer Aggregator
·
refine-loop cycle 7: +articles/deals 08-20 09:13 7d71042 →