[object Object]

← back to Commercialrealestate

CRCP scope Cycle 4: derive city from address for direct-listings (0%->correctly scoped); fha-loans statewide (national HUD data); event-driven honest empty-state for scoped property grids that filter to 0

a9088ee9ea1e5ee5af7fd299ca7be9837ca4d1cf · 2026-08-20 15:12:02 -0700 · Steve

Files touched

Diff

commit a9088ee9ea1e5ee5af7fd299ca7be9837ca4d1cf
Author: Steve <steve@designerwallcoverings.com>
Date:   Thu Aug 20 15:12:02 2026 -0700

    CRCP scope Cycle 4: derive city from address for direct-listings (0%->correctly scoped); fha-loans statewide (national HUD data); event-driven honest empty-state for scoped property grids that filter to 0
---
 public/crcp-scope.js | 97 ++++++++++++++++++++++++++++++++++++++--------------
 1 file changed, 71 insertions(+), 26 deletions(-)

diff --git a/public/crcp-scope.js b/public/crcp-scope.js
index ddebebf..fc1e25a 100644
--- a/public/crcp-scope.js
+++ b/public/crcp-scope.js
@@ -13,7 +13,7 @@
   var CITY = window.CRCP_CITIES.cityFromHost(location.hostname);
   if (!CITY) return;                                        // base crcp / recities / localhost -> no scope
 
-  var scoped = { ranked: 0, total: 0 };
+  var scoped = { ranked: 0, total: 0, arrBefore: 0, arrAfter: 0, arrMatched: false };
 
   // Property-level grids that carry a flat top-level .city and should be city-filtered on a subdomain.
   // (rent-rolls is intentionally excluded — its city is nested and it's a single sample row.)
@@ -21,10 +21,32 @@
     { re: /\/api\/closed-sales/, key: "rows" },
     { re: /\/api\/direct-listings/, key: "listings" },
     { re: /\/data\/listings\.json/, key: "listings" },
-    { re: /\/data\/fha-loans\.json/, key: "rows" },
     { re: /\/data\/valley-pools\.json/, key: "homes" }
+    // fha-loans.json is intentionally NOT scoped: it's ~93% non-California NATIONAL HUD
+    // reference data (498/7404 rows mention CA), so per-city scoping yields 0 rows on
+    // most CRCP cities and a misleading "no listings here" empty-state. Treated statewide
+    // like the broker/lender directories instead (see DIRECTORY_PAGES below).
   ];
 
+  // Some property feeds ship item.city === null but embed the city in item.address
+  // ("STREET, CITY, ST ZIP"). Derive it: the comma-segment right before the "ST ZIP"
+  // tail is the city. No parseable tail -> null (row fails closed = drops on a city host).
+  function deriveCity(item) {
+    if (item && item.city != null && String(item.city).replace(/\s/g, "") !== "") return item.city;
+    if (!item || !item.address) return null;
+    var parts = String(item.address).split(",");
+    for (var j = 0; j < parts.length; j++) parts[j] = parts[j].replace(/^\s+|\s+$/g, "");
+    parts = parts.filter(function (s) { return !!s; });
+    for (var k = parts.length - 1; k >= 1; k--) {
+      if (/^[A-Za-z]{2}\s+\d{5}/.test(parts[k])) return parts[k - 1] || null;
+    }
+    return null;
+  }
+
+  // Re-run the banner/empty-state the instant a scoped fetch resolves (event-driven, not a
+  // fixed timer) so the empty-state never depends on data arriving before a magic 2500ms.
+  function schedulePaint() { setTimeout(function () { if (document.body) paintBanner(); }, 0); }
+
   var _fetch = window.fetch;
   window.fetch = function (input, init) {
     var url = (typeof input === "string") ? input : (input && input.url) || "";
@@ -37,6 +59,7 @@
             d.ranked = d.ranked.filter(function (x) { return window.CRCP_CITIES.sameCity(x.city, CITY); });
             scoped.ranked = d.ranked.length;
             if (d.meta) d.meta.market = CITY.label;          // sub-header shows the scoped market name
+            schedulePaint();
           }
           return new Response(JSON.stringify(d), { status: 200, headers: { "Content-Type": "application/json" } });
         }).catch(function () { return r; });                 // parse failure -> pass original through
@@ -56,7 +79,13 @@
         return p.then(function (key) {
           return function (r) {
             return r.clone().json().then(function (d) {
-              if (d && Array.isArray(d[key])) d[key] = d[key].filter(function (item) { return window.CRCP_CITIES.sameCity(item.city, CITY); });
+              if (d && Array.isArray(d[key])) {
+                scoped.arrBefore = d[key].length;
+                scoped.arrMatched = true;
+                d[key] = d[key].filter(function (item) { return window.CRCP_CITIES.sameCity(deriveCity(item), CITY); });
+                scoped.arrAfter = d[key].length;
+                schedulePaint();
+              }
               return new Response(JSON.stringify(d), { status: 200, headers: { "Content-Type": "application/json" } });
             }).catch(function () { return r; });               // parse failure -> pass original through
           };
@@ -67,32 +96,48 @@
   };
 
   // Scope banner + empty-state, injected once the DOM is ready (does not touch app JS).
+  var EMPTY_CSS = "margin:22px;padding:18px 20px;border:1px solid #24487e;border-radius:12px;" +
+    "background:#0f1b30;color:#c7d5ea;font:14px/1.5 -apple-system,sans-serif;text-align:center";
+  var DIRECTORY_PAGES = ["/brokers.html", "/broker-grid.html", "/firms.html", "/lenders.html",
+    "/loan-officers.html", "/lending.html", "/gov-agents.html", "/licensed-agents.html",
+    "/licensed.html", "/residential-brokers.html", "/linkedin.html",
+    "/fha-leads.html", "/deals-flow.html", "/fha-loans.html"];   // fha = national HUD reference, statewide
+
+  function emptyNote(msg) {
+    var el = document.createElement("div");
+    el.setAttribute("data-crcp-empty", "1");
+    el.style.cssText = EMPTY_CSS;
+    el.innerHTML = msg + ' <a href="https://crcp.agentabrams.com/" style="color:#9ecbff">Browse all markets &rarr;</a>';
+    return el;
+  }
+
+  // Called twice (DOMContentLoaded + a 2500ms re-check) because the grid data loads async.
+  // Banner is created once; the empty-state is re-evaluated on EVERY call (its own
+  // [data-crcp-empty] guard prevents doubling) so the delayed re-check can add it after
+  // the fetch resolves — the early-return used to sit above this and killed it.
   function paintBanner() {
-    if (document.getElementById("crcp-scope-banner")) return;
-    var bar = document.createElement("div");
-    bar.id = "crcp-scope-banner";
-    bar.style.cssText = "position:sticky;top:0;z-index:9999;display:flex;gap:12px;align-items:center;" +
-      "justify-content:center;padding:9px 16px;background:#12305a;color:#eaf1fb;font:13px/1.4 -apple-system," +
-      "BlinkMacSystemFont,'Segoe UI',Roboto,Helvetica,Arial,sans-serif;border-bottom:1px solid #24487e";
-    var n = scoped.ranked;
-    var DIRECTORY_PAGES = ["/brokers.html", "/broker-grid.html", "/firms.html", "/lenders.html",
-      "/loan-officers.html", "/lending.html", "/gov-agents.html", "/licensed-agents.html",
-      "/licensed.html", "/residential-brokers.html", "/linkedin.html"];
     var isDirectory = DIRECTORY_PAGES.indexOf(location.pathname) !== -1;
-    bar.innerHTML =
-      '<span>Scoped to <b>' + CITY.label + '</b>' + (n ? ' · ' + n + ' listing' + (n === 1 ? '' : 's') : '') + '</span>' +
-      (isDirectory ? '<span style="color:#8aa2c4;font-size:12px">· directories are statewide</span>' : '') +
-      '<a href="https://crcp.agentabrams.com/" style="color:#9ecbff;text-decoration:none;border:1px solid #2f5da3;border-radius:8px;padding:3px 10px">View all markets →</a>' +
-      '<a href="https://recities.crcp.agentabrams.com/" style="color:#9ecbff;text-decoration:none;border:1px solid #2f5da3;border-radius:8px;padding:3px 10px">All cities</a>';
-    document.body.insertBefore(bar, document.body.firstChild);
+    var bar = document.getElementById("crcp-scope-banner");
+    if (!bar) {
+      bar = document.createElement("div");
+      bar.id = "crcp-scope-banner";
+      bar.style.cssText = "position:sticky;top:0;z-index:9999;display:flex;gap:12px;align-items:center;" +
+        "justify-content:center;padding:9px 16px;background:#12305a;color:#eaf1fb;font:13px/1.4 -apple-system," +
+        "BlinkMacSystemFont,'Segoe UI',Roboto,Helvetica,Arial,sans-serif;border-bottom:1px solid #24487e";
+      var n = scoped.ranked;
+      bar.innerHTML =
+        '<span>Scoped to <b>' + CITY.label + '</b>' + (n ? ' · ' + n + ' listing' + (n === 1 ? '' : 's') : '') + '</span>' +
+        (isDirectory ? '<span style="color:#8aa2c4;font-size:12px">· directories are statewide</span>' : '') +
+        '<a href="https://crcp.agentabrams.com/" style="color:#9ecbff;text-decoration:none;border:1px solid #2f5da3;border-radius:8px;padding:3px 10px">View all markets →</a>' +
+        '<a href="https://recities.crcp.agentabrams.com/" style="color:#9ecbff;text-decoration:none;border:1px solid #2f5da3;border-radius:8px;padding:3px 10px">All cities</a>';
+      document.body.insertBefore(bar, document.body.firstChild);
+    }
 
-    if (scoped.ranked === 0 && scoped.total > 0) {          // scoped after data loaded, but city has none
-      var note = document.createElement("div");
-      note.style.cssText = "margin:22px;padding:18px 20px;border:1px solid #24487e;border-radius:12px;" +
-        "background:#0f1b30;color:#c7d5ea;font:14px/1.5 -apple-system,sans-serif;text-align:center";
-      note.innerHTML = "No CRCP listings in <b>" + CITY.label + "</b> yet. " +
-        '<a href="https://crcp.agentabrams.com/" style="color:#9ecbff">Browse all markets →</a>';
-      bar.insertAdjacentElement("afterend", note);
+    if (document.querySelector("[data-crcp-empty]")) return;   // empty-state already shown, don't double
+    if (scoped.ranked === 0 && scoped.total > 0) {             // main grid scoped but city has none
+      bar.insertAdjacentElement("afterend", emptyNote("No CRCP listings in <b>" + CITY.label + "</b> yet."));
+    } else if (scoped.arrMatched && scoped.arrAfter === 0 && scoped.arrBefore > 0 && !isDirectory) {
+      bar.insertAdjacentElement("afterend", emptyNote("No listings in <b>" + CITY.label + "</b> for this view yet."));
     }
   }
   // ranked.json loads async; re-check the empty-state shortly after boot so the count is accurate.

← c4ea663 CRCP: inline-city SSO — one hub login reaches all cities inl  ·  back to Commercialrealestate  ·  auto-data-snapshot: 2026-08-20T15:43:22 (2 data files) — dat 111d4f3 →