[object Object]

← back to Homesonspec

fulton: parse bed/bath RANGES correctly (contrarian fix) — recovers 36 dropped homes

e3c33f7b11f9fef31c42439e180c6faecdc06b56 · 2026-07-28 22:25:57 -0700 · Steve Abrams

Cody's gate caught 36 real AZ homes (valid price+sqft) being validation-rejected as '34/56/57-
bedroom' homes. Root cause: posNum() stripped the dash from flex-bedroom ranges like '3-4' -> '34'.
Added firstNum() to take the base value of a range. Verified: Fulton 166 -> 202 homes, beds now
3-6, 0 rejects (was 36). Also killed dead code (Math.min(1,PAGE_LIMIT) always=1).

Files touched

Diff

commit e3c33f7b11f9fef31c42439e180c6faecdc06b56
Author: Steve Abrams <steve@designerwallcoverings.com>
Date:   Tue Jul 28 22:25:57 2026 -0700

    fulton: parse bed/bath RANGES correctly (contrarian fix) — recovers 36 dropped homes
    
    Cody's gate caught 36 real AZ homes (valid price+sqft) being validation-rejected as '34/56/57-
    bedroom' homes. Root cause: posNum() stripped the dash from flex-bedroom ranges like '3-4' -> '34'.
    Added firstNum() to take the base value of a range. Verified: Fulton 166 -> 202 homes, beds now
    3-6, 0 rejects (was 36). Also killed dead code (Math.min(1,PAGE_LIMIT) always=1).
---
 collectors/fulton-homes/src/index.ts | 20 +++++++++++++++-----
 1 file changed, 15 insertions(+), 5 deletions(-)

diff --git a/collectors/fulton-homes/src/index.ts b/collectors/fulton-homes/src/index.ts
index af071582..424eebe1 100644
--- a/collectors/fulton-homes/src/index.ts
+++ b/collectors/fulton-homes/src/index.ts
@@ -87,6 +87,15 @@ const posNum = (v: unknown): number | null => {
   const n = typeof v === "number" ? v : typeof v === "string" ? Number(String(v).replace(/[^0-9.]/g, "")) : NaN;
   return Number.isFinite(n) && n > 0 ? n : null;
 };
+// First positive number in a string, or null. Handles bed/bath RANGES ("3-4", "5 - 6")
+// by taking the base (first) value — posNum() would strip the dash and read "3-4" as 34,
+// producing bogus 34-bedroom homes the validator rejects (contrarian fix, verified 2026-07-28).
+const firstNum = (v: unknown): number | null => {
+  const m = String(v ?? "").match(/\d+(?:\.\d+)?/);
+  if (!m) return null;
+  const n = Number(m[0]);
+  return Number.isFinite(n) && n > 0 ? n : null;
+};
 const str = (v: unknown): string | null => {
   if (v == null) return null;
   const s = String(v).trim();
@@ -191,8 +200,8 @@ function parseSpecRows(html: string): SpecRow[] {
       price,
       available: price !== null,
       status,
-      beds: posNum(text(bedsC ?? "")),
-      baths: posNum(text(bathC ?? "")),
+      beds: firstNum(text(bedsC ?? "")),
+      baths: firstNum(text(bathC ?? "")),
     });
   }
   return rows;
@@ -216,9 +225,10 @@ export const fultonHomesAdapter: SourceAdapter = {
       return;
     }
     const fetcher = new LiveFetcher(ctx.registry);
-    // The GetSpecs feed is single-call (returns the whole statewide inventory);
-    // FULTON_PAGE_LIMIT is a safety cap, so we fetch at most one page.
-    for (let pageNo = 0; pageNo < Math.max(1, Math.min(1, PAGE_LIMIT)); pageNo++) {
+    // The GetSpecs feed is single-call (returns the whole statewide inventory in one response),
+    // so there is exactly one page regardless of FULTON_PAGE_LIMIT (which stays for interface
+    // parity with the other adapters). Fetch it once. [was Math.min(1,PAGE_LIMIT) dead code]
+    for (let pageNo = 0; pageNo < 1; pageNo++) {
       let page: RawPage;
       try {
         page = await fetcher.fetch(GETSPECS_URL);

← d9a0af8c ashton-woods: tighten image blocklist + purge residual pollu  ·  back to Homesonspec  ·  geocode: Census-batch enrichment for no-geo homes (yolo iter 139ce339 →