← back to Commercialrealestate
CRE viewer: neighborhood rent estimate (std + proforma from Census median gross rent × units) per card + Pro-forma Rent ROI sort, with implausible-yield (>18%) guard flagging bad unit counts
5aaf087f9d63f4c420e3a744e092361d31beb1b4 · 2026-06-28 16:12:45 -0700 · Steve
Files touched
Diff
commit 5aaf087f9d63f4c420e3a744e092361d31beb1b4
Author: Steve <steve@designerwallcoverings.com>
Date: Sun Jun 28 16:12:45 2026 -0700
CRE viewer: neighborhood rent estimate (std + proforma from Census median gross rent × units) per card + Pro-forma Rent ROI sort, with implausible-yield (>18%) guard flagging bad unit counts
---
public/index.html | 23 ++++++++++++++++++++++-
1 file changed, 22 insertions(+), 1 deletion(-)
diff --git a/public/index.html b/public/index.html
index e20c3c0..676a3b4 100644
--- a/public/index.html
+++ b/public/index.html
@@ -167,6 +167,7 @@
<option value="priceDesc">Price ↓</option>
<option value="cashNeeded">Cash Needed ↑</option>
<option value="unitsDesc">Units ↓</option>
+ <option value="rentRoi">Pro-forma Rent ROI ↓</option>
</select>
<div style="margin-top:10px"><label class="small">Cards per row <span class="rangeval" id="densVal">3</span></label>
<input type="range" id="density" min="1" max="6" step="1" value="3"></div>
@@ -220,6 +221,24 @@ function demoLine(p){
if(d.population) bits.push(`${Math.round(d.population/1000)}k pop`);
return bits.length ? `<div class="small" title="US Census ACS 2022 — ${p.city} (ZCTA ${d.zcta})">📊 ${bits.join(' · ')}</div>` : '';
}
+// Neighborhood-based rent estimate from Census median gross rent. std = market median × units
+// (conservative in-place proxy); proforma = +12% market upside. Honest estimate, NOT an appraisal/1007.
+function rentEstimate(p){
+ const d = window.DEMO[(p.city||'').toLowerCase()]; if(!d || !d.median_gross_rent) return null;
+ const base = d.median_gross_rent, units = p.units || 1;
+ const std = Math.round(base*units), pro = Math.round(std*1.12);
+ const annualPro = pro*12;
+ return { std, pro, base, units, zcta:d.zcta, grossYieldPro: p.price? +(100*annualPro/p.price).toFixed(2):null };
+}
+function rentLine(p){
+ const r = rentEstimate(p); if(!r) return '';
+ // Plausibility guard: LA gross yields run ~5-10%. >18% almost always means a bad units count, not a deal.
+ const fishy = r.grossYieldPro && r.grossYieldPro > 18;
+ const yld = r.grossYieldPro ? ` · ${r.grossYieldPro}% gross yld (pro)${fishy?` <span class="b warn" title="Implausibly high — likely a bad unit count on the listing; verify.">⚠ verify units</span>`:''}` : '';
+ return `<div class="small" title="Estimate from Census median gross rent ($${r.base.toLocaleString()}/unit, ${p.city} ZCTA ${r.zcta}) × ${r.units} unit${r.units>1?'s':''}. Not an appraisal — DSCR screen only.">💵 Est. rent: <b style="color:var(--ink)">$${r.std.toLocaleString()}/mo</b> std · <b style="color:var(--acc)">$${r.pro.toLocaleString()}/mo</b> proforma${yld}</div>`;
+}
+// For ranking, treat implausible (>18%) pro-forma yields as low-confidence so bad-units outliers don't top the list.
+function rentRoiScore(p){ const r=rentEstimate(p); if(!r||!r.grossYieldPro) return -1e9; return r.grossYieldPro>18 ? -1 : r.grossYieldPro; }
// Rich filter state
const F = { types:new Set(), firms:new Set(), cityQuery:'', priceMin:null, priceMax:null,
capMin:0, unitsMin:null, yearMin:null, status:'all', afford:false };
@@ -277,6 +296,7 @@ function card(p){
${p.qwen?.risks?.length?`<div><span class="small">Risks</span><ul class="lst">${p.qwen.risks.map(r=>`<li>${r}</li>`).join('')}</ul></div>`:''}
${p.upside_note?`<div class="small">💡 ${p.upside_note}</div>`:''}
${demoLine(p)}
+ ${rentLine(p)}
<div class="src"><a href="${p.source}" target="_blank" rel="noopener noreferrer">View listing ↗</a> · <button class="lcbtn" data-id="${p.id}" title="Pull fresh comps from LoopNet via a cloud browser (~$0.03)">🔄 Live comps</button> · <button class="histbtn" data-id="${p.id}" data-address="${(p.address||'').replace(/"/g,'"')}" data-city="${(p.city||'').replace(/"/g,'"')}" data-zip="${p.zip||''}" title="LA County assessor + permit + film history (free)">📜 Property history</button></div>
<div class="lcout" id="lc-${p.id}"></div>
<div class="histout" id="hist-${p.id}"></div>
@@ -312,7 +332,8 @@ const SORTS={ composite:(a,b)=>b.composite-a.composite, coc:(a,b)=>num(b.finance
yield:(a,b)=>num(b.finance.grossYield)-num(a.finance.grossYield),
cap:(a,b)=>num(b.cap_rate)-num(a.cap_rate), dscr:(a,b)=>num(b.finance.dscr)-num(a.finance.dscr),
priceAsc:(a,b)=>a.price-b.price, priceDesc:(a,b)=>b.price-a.price,
- cashNeeded:(a,b)=>a.finance.cashNeeded-b.finance.cashNeeded, unitsDesc:(a,b)=>num(b.units)-num(a.units) };
+ cashNeeded:(a,b)=>a.finance.cashNeeded-b.finance.cashNeeded, unitsDesc:(a,b)=>num(b.units)-num(a.units),
+ rentRoi:(a,b)=>rentRoiScore(b)-rentRoiScore(a) };
const num=v=>v==null?-1e9:v;
function render(){
← 9f0bd7e Wire sfr-bankstmt segment to cre.sfr + add fetch-sfr-agents.
·
back to Commercialrealestate
·
CRE: transaction-party entity model — title/escrow/insurance 50e36fe →