[object Object]

← 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,'&quot;')}" data-city="${(p.city||'').replace(/"/g,'&quot;')}" 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 →