← back to Commercialrealestate
CRE: warrantability classifier — FHA-list match (name + street/zip) primary, heuristic text flags (condotel/cash-only/litigation/investor/mixed-use/new-construction) secondary; honest proxy labeling
b27a82b99bc28e0f016b39f723b9b662f92e89a1 · 2026-06-28 06:18:03 -0700 · Steve Abrams
Files touched
A scripts/classify-warrantability.js
Diff
commit b27a82b99bc28e0f016b39f723b9b662f92e89a1
Author: Steve Abrams <steve@designerwallcoverings.com>
Date: Sun Jun 28 06:18:03 2026 -0700
CRE: warrantability classifier — FHA-list match (name + street/zip) primary, heuristic text flags (condotel/cash-only/litigation/investor/mixed-use/new-construction) secondary; honest proxy labeling
---
scripts/classify-warrantability.js | 164 +++++++++++++++++++++++++++++++++++++
1 file changed, 164 insertions(+)
diff --git a/scripts/classify-warrantability.js b/scripts/classify-warrantability.js
new file mode 100644
index 0000000..bfe2453
--- /dev/null
+++ b/scripts/classify-warrantability.js
@@ -0,0 +1,164 @@
+// classify-warrantability.js — warrantability classifier for LA condos (FHA/VA-approval-based PROXY).
+//
+// HONEST LABELING (hard rule): this returns a PROXY, NOT a lender-verified Fannie/Freddie warrantability
+// result. Primary signal = match against the public HUD FHA-approved condo list (data/fha-approved-condos.json).
+// Secondary = heuristic text flags (condotel / short-term-rental, "cash only" / "not warrantable" /
+// "investor", high commercial ratio, new-construction <% sold). Every signal is labeled with its basis.
+//
+// API:
+// const { classify, loadFhaList } = require('./classify-warrantability');
+// const fha = loadFhaList();
+// classify({ project_name, address, city, zip, listing_text, commercial_ratio, pct_sold }, fha)
+// -> { status, label, source, signals: [{ kind, level, basis, note }] }
+//
+// status ∈ fha_approved | fha_expired | not_listed | heuristic_flag
+// fha_approved — on the FHA list, currently approved (financing-eligible proxy)
+// fha_expired — on the FHA list but approval lapsed (was eligible; re-cert needed)
+// heuristic_flag — NOT on the list AND a hard non-warrantable text signal fired (flag for review)
+// not_listed — not on the FHA list, no hard flags (unknown; flag for lender verification)
+
+'use strict';
+const fs = require('fs');
+const path = require('path');
+
+const PROXY_LABEL = 'FHA/VA-approval-based proxy, NOT lender-verified Fannie/Freddie warrantability.';
+
+function loadFhaList(file) {
+ file = file || path.join(__dirname, '..', 'data', 'fha-approved-condos.json');
+ const { condos } = JSON.parse(fs.readFileSync(file, 'utf8'));
+ // Index by normalized project name and by zip for fast lookup.
+ const byName = new Map();
+ const byZip = new Map();
+ for (const c of condos) {
+ const nk = norm(c.project_name);
+ if (nk) { if (!byName.has(nk)) byName.set(nk, []); byName.get(nk).push(c); }
+ if (c.zip) { if (!byZip.has(c.zip)) byZip.set(c.zip, []); byZip.get(c.zip).push(c); }
+ }
+ return { condos, byName, byZip };
+}
+
+const norm = s => (s || '').toLowerCase().replace(/\b(condominiums?|condos?|owners?|hoa|association|apartments?|the)\b/g, '')
+ .replace(/[^a-z0-9 ]/g, ' ').replace(/\s+/g, ' ').trim();
+
+// FHA-list match: try exact normalized project-name match (optionally zip-scoped), then a
+// street-number + zip match against the FHA record's own address. Returns the best FHA record or null.
+function fhaMatch(condo, fha) {
+ const want = norm(condo.project_name);
+ // 1) project-name match, prefer same-zip + approved
+ if (want && want.length > 2 && fha.byName.has(want)) {
+ const cands = fha.byName.get(want)
+ .filter(c => !condo.zip || !c.zip || c.zip === condo.zip);
+ const pick = pickBest(cands);
+ if (pick) return { rec: pick, how: 'project-name match' };
+ }
+ // 2) street-number + zip match against FHA address (catches address-only listings)
+ const num = (condo.address || '').match(/^\s*(\d+)/);
+ if (num && condo.zip && fha.byZip.has(condo.zip)) {
+ const cands = fha.byZip.get(condo.zip).filter(c => (c.address || '').trim().startsWith(num[1] + ' '));
+ const pick = pickBest(cands);
+ if (pick) return { rec: pick, how: 'street-number + zip match' };
+ }
+ return null;
+}
+
+function pickBest(cands) {
+ if (!cands || !cands.length) return null;
+ // Prefer currently-approved over expired.
+ return cands.sort((a, b) =>
+ (a.warrant_signal === 'fha_approved' ? 0 : 1) - (b.warrant_signal === 'fha_approved' ? 0 : 1))[0];
+}
+
+// Heuristic non-warrantable text flags. Each returns a signal when its pattern hits the listing text.
+const HEURISTICS = [
+ { kind: 'condotel', level: 'hard',
+ re: /\b(condo[\s-]?tel|hotel[\s-]?condo|resort condominium|short[\s-]?term rental|airbnb|vrbo|nightly rental|transient occupancy)\b/i,
+ note: 'condotel / short-term-rental operation — typically non-warrantable' },
+ { kind: 'cash_only', level: 'hard',
+ re: /\b(cash[\s-]?only|cash buyers? only|no financing|not warrantable|non[\s-]?warrantable|unwarrantable|will not qualify for financing)\b/i,
+ note: 'listing states cash-only / not-warrantable' },
+ { kind: 'litigation', level: 'hard',
+ re: /\b(pending litigation|hoa litigation|active lawsuit|construction defect (litigation|suit))\b/i,
+ note: 'HOA / construction-defect litigation — common warrantability disqualifier' },
+ { kind: 'investor_heavy', level: 'soft',
+ re: /\b(investor[\s-]?owned|high (investor|rental) concentration|majority (rented|tenant[\s-]?occupied)|low owner[\s-]?occupancy)\b/i,
+ note: 'high investor/rental concentration noted — may fail owner-occupancy thresholds' },
+ { kind: 'commercial_mixed', level: 'soft',
+ re: /\b(mixed[\s-]?use|ground[\s-]?floor (retail|commercial)|commercial space below|live[\s-]?work)\b/i,
+ note: 'mixed-use / commercial component — high commercial ratio can be non-warrantable' },
+ { kind: 'new_construction', level: 'soft',
+ re: /\b(new construction|pre[\s-]?construction|under construction|to be built|brand new development|phase \d+ now selling)\b/i,
+ note: 'new-construction / early-phase — pre-sale % can fail warrantability until enough units close' }
+];
+
+function classify(condo, fha) {
+ const signals = [];
+ const text = (condo.listing_text || '') + ' ' + (condo.project_name || '');
+
+ // Primary: FHA-list match.
+ const m = fhaMatch(condo, fha);
+ if (m) {
+ const r = m.rec;
+ signals.push({
+ kind: 'fha_list', level: 'primary',
+ basis: 'HUD FHA-approved condo list (' + m.how + ')',
+ note: `${r.project_name} — HUD ${r.status}` +
+ (r.expiration_date ? `, expires ${r.expiration_date}` : '') +
+ (r.fha_concentration ? `, FHA concentration ${r.fha_concentration}` : '')
+ });
+ }
+
+ // Secondary: heuristic flags (always evaluated, for transparency).
+ for (const h of HEURISTICS) {
+ if (h.re.test(text)) signals.push({ kind: h.kind, level: h.level, basis: 'listing-text heuristic', note: h.note });
+ }
+ // Numeric heuristics when fields are present.
+ if (condo.commercial_ratio != null && condo.commercial_ratio > 0.35) {
+ signals.push({ kind: 'commercial_ratio', level: 'hard', basis: 'computed commercial ratio',
+ note: `commercial space ${Math.round(condo.commercial_ratio * 100)}% (>35% non-warrantable threshold)` });
+ }
+ if (condo.pct_sold != null && condo.pct_sold < 0.5) {
+ signals.push({ kind: 'pct_sold', level: 'soft', basis: 'computed pre-sale %',
+ note: `only ${Math.round(condo.pct_sold * 100)}% of units sold (<50% — typically not yet warrantable)` });
+ }
+
+ // Resolve status. FHA match dominates; else hard heuristic flag; else not_listed.
+ let status, source;
+ if (m && m.rec.warrant_signal === 'fha_approved') {
+ status = 'fha_approved';
+ source = `HUD FHA list (${m.rec.condo_id || '?'} ${m.rec.submission || ''}, Approved` +
+ (m.rec.expiration_date ? `, exp ${m.rec.expiration_date}` : '') + ')';
+ } else if (m && m.rec.warrant_signal === 'fha_expired') {
+ status = 'fha_expired';
+ source = `HUD FHA list (${m.rec.condo_id || '?'} ${m.rec.submission || ''}, Expired` +
+ (m.rec.expiration_date ? ` ${m.rec.expiration_date}` : '') + ')';
+ } else if (signals.some(s => s.level === 'hard')) {
+ status = 'heuristic_flag';
+ source = 'heuristic: ' + signals.filter(s => s.level === 'hard').map(s => s.kind).join(', ');
+ } else {
+ status = 'not_listed';
+ source = 'not on FHA list; no hard flags — verify with lender';
+ }
+
+ return { status, label: PROXY_LABEL, source, signals };
+}
+
+module.exports = { classify, loadFhaList, fhaMatch, PROXY_LABEL };
+
+// CLI: pipe a JSON condo, or run with a built-in sample set, to see classifications.
+if (require.main === module) {
+ const fha = loadFhaList();
+ const samples = [
+ { project_name: '1200 NORTH', address: '1200 N FLORES ST', city: 'WEST HOLLYWOOD', zip: '90069', listing_text: 'Gorgeous 2BR condo in prime WeHo.' },
+ { project_name: 'Some Random Tower', address: '500 Nowhere Ave', city: 'Los Angeles', zip: '90017', listing_text: 'CASH ONLY — not warrantable, investor special, ground-floor retail below.' },
+ { project_name: 'Marina Pointe', address: '4267 Marina City Dr', city: 'Marina Del Rey', zip: '90292', listing_text: 'Luxury high-rise, short-term rental / airbnb friendly building.' },
+ { project_name: 'Unknown Condos', address: '123 Main St', city: 'Glendale', zip: '91205', listing_text: 'Bright move-in ready 1BR, low HOA.' },
+ { project_name: '10919 BLIX CONDOMINIUMS', address: '10925 BLIX STREET', city: 'NORTH HOLLYWOOD', zip: '91602', listing_text: 'Updated unit.' }
+ ];
+ for (const s of samples) {
+ const r = classify(s, fha);
+ console.log(`\n${s.project_name} (${s.city} ${s.zip})`);
+ console.log(` STATUS: ${r.status} [${r.source}]`);
+ r.signals.forEach(sig => console.log(` • [${sig.level}/${sig.kind}] ${sig.note} (${sig.basis})`));
+ }
+ console.log('\n' + PROXY_LABEL);
+}
← 848cc19 CRE: add condo + warrantability schema (fha_condo ref table,
·
back to Commercialrealestate
·
CRE: graphics page (broker leaderboard, firm market-share, F 37b95ec →