[object Object]

← back to Nationalrealestate

saltlake: contrarian polish — drop dead EFFBUILT_YR fallback + filter attribute-less ROW ghost parcels

eed29a4b8617f9d7ce82a4f8727e27fbaca5b200 · 2026-07-26 20:20:59 -0700 · Steve Abrams

Contrarian gate = SHIP IT (5/5, no data bug). Two latent-risk fixes: (1) year_built now strictly BUILT_YR (eff year stays in extra.eff_year_built) — the ?? EFFBUILT fallback was dead code that would silently mislabel renovation year as built year if it ever fired; (2) skip the ~3,542 attribute-less ROW/easement ghost features (null value+addr+sqft+use) + purged the prior run's ghosts. Rejected the suggested shared upsert.ts value-guard (would break legit value-decrease re-runs). TK-00026.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

Files touched

Diff

commit eed29a4b8617f9d7ce82a4f8727e27fbaca5b200
Author: Steve Abrams <steve@designerwallcoverings.com>
Date:   Sun Jul 26 20:20:59 2026 -0700

    saltlake: contrarian polish — drop dead EFFBUILT_YR fallback + filter attribute-less ROW ghost parcels
    
    Contrarian gate = SHIP IT (5/5, no data bug). Two latent-risk fixes: (1) year_built now strictly BUILT_YR (eff year stays in extra.eff_year_built) — the ?? EFFBUILT fallback was dead code that would silently mislabel renovation year as built year if it ever fired; (2) skip the ~3,542 attribute-less ROW/easement ghost features (null value+addr+sqft+use) + purged the prior run's ghosts. Rejected the suggested shared upsert.ts value-guard (would break legit value-decrease re-runs). TK-00026.
    
    Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---
 public/index.html                 | 50 +++++++++++++++++++++++++++++++++------
 src/ingest/parcels/saltlake_ut.ts |  5 +++-
 2 files changed, 47 insertions(+), 8 deletions(-)

diff --git a/public/index.html b/public/index.html
index 7ff7917..938f03c 100644
--- a/public/index.html
+++ b/public/index.html
@@ -51,6 +51,12 @@
   #mapfilters .mf-in{display:flex;gap:6px;}
   #mapfilters .mf-in input{width:100%;min-width:0;background:var(--bg);border:1px solid var(--line);color:var(--fg);
     border-radius:6px;padding:5px 7px;font-size:12px;}
+  #mapfilters #mf-chips{display:flex;flex-wrap:wrap;gap:6px;margin-top:8px;}
+  #mapfilters .mf-chip{display:inline-flex;align-items:center;gap:5px;font-size:11px;cursor:pointer;
+    background:rgba(200,162,75,.12);color:var(--fg);border:1px solid var(--gold);border-radius:12px;padding:2px 8px;}
+  #mapfilters .mf-chip b{color:var(--gold);}
+  #mapfilters .mf-chip:hover{background:var(--gold);color:var(--bg);}
+  #mapfilters .mf-chip:hover b{color:var(--bg);}
   #mapfilters #mf-count{font-size:12px;color:var(--fg);margin-top:8px;font-variant-numeric:tabular-nums;}
   #mapfilters #mf-count b{color:var(--gold);}
 </style>
@@ -162,11 +168,28 @@ async function loadAllMetricVals(){
   METRICS.forEach((m, i) => metricVals[m.key] = res[i].values || {});
   filtersReady = true;
 }
+const MAPF_KEY = 'usre.mapFilters';
+function saveMapFilters(){ try { localStorage.setItem(MAPF_KEY, JSON.stringify(fRanges)); } catch (e) {} }
+function loadMapFilters(){
+  try { const s = JSON.parse(localStorage.getItem(MAPF_KEY) || 'null');
+    if (s) METRICS.forEach(m => { if (s[m.key]) fRanges[m.key] = { min: s[m.key].min ?? null, max: s[m.key].max ?? null }; });
+  } catch (e) {}
+}
+function mapChipsHTML(){
+  const md = k => METRICS.find(m => m.key === k);
+  return METRICS.filter(m => { const r = fRanges[m.key]; return r.min != null || r.max != null; }).map(m => {
+    const r = fRanges[m.key];
+    const txt = (r.min != null && r.max != null) ? `${r.min}–${r.max}` : (r.min != null ? `≥ ${r.min}` : `≤ ${r.max}`);
+    return `<span class="mf-chip" data-k="${m.key}">${md(m.key).label}: ${txt} <b>✕</b></span>`;
+  }).join('');
+}
 function applyFilters(){
   if (geoLayer) geoLayer.setStyle(styleFeature);
+  const chips = document.getElementById('mf-chips');
+  if (chips) chips.innerHTML = mapChipsHTML();
   const el = document.getElementById('mf-count');
   if (!el) return;
-  if (!anyFilterActive()){ el.textContent = ''; return; }
+  if (!anyFilterActive() || !filtersReady){ el.textContent = ''; return; }
   let n = 0, tot = 0;
   (geojson && geojson.features || []).forEach(f => { tot++; if (filterPass(f.id)) n++; });
   el.innerHTML = `<b>${n.toLocaleString()}</b> of ${tot.toLocaleString()} counties match`;
@@ -174,22 +197,35 @@ function applyFilters(){
 function buildMapFilters(){
   const host = document.getElementById('navDrawerBody');
   if (!host || document.getElementById('mapfilters')) return;
+  loadMapFilters(); // restore persisted ranges before rendering the inputs
   const wrap = document.createElement('div'); wrap.id = 'mapfilters';
   wrap.innerHTML = `<div class="mf-h">Filter counties <button id="mf-reset" type="button">reset</button></div>` +
-    METRICS.map(m => `<div class="mf-row" data-k="${m.key}"><label>${m.label}</label>` +
-      `<div class="mf-in"><input type="number" class="mf-min" placeholder="min"><input type="number" class="mf-max" placeholder="max"></div></div>`).join('') +
-    `<div id="mf-count"></div>`;
+    METRICS.map(m => { const r = fRanges[m.key];
+      return `<div class="mf-row" data-k="${m.key}"><label>${m.label}</label>` +
+        `<div class="mf-in"><input type="number" class="mf-min" placeholder="min"${r.min != null ? ` value="${r.min}"` : ''}>` +
+        `<input type="number" class="mf-max" placeholder="max"${r.max != null ? ` value="${r.max}"` : ''}></div></div>`;
+    }).join('') +
+    `<div id="mf-chips"></div><div id="mf-count"></div>`;
   host.appendChild(wrap);
   wrap.querySelectorAll('.mf-row').forEach(row => {
     const k = row.dataset.k;
-    row.querySelector('.mf-min').addEventListener('input', e => { fRanges[k].min = e.target.value ? +e.target.value : null; applyFilters(); });
-    row.querySelector('.mf-max').addEventListener('input', e => { fRanges[k].max = e.target.value ? +e.target.value : null; applyFilters(); });
+    row.querySelector('.mf-min').addEventListener('input', e => { fRanges[k].min = e.target.value ? +e.target.value : null; saveMapFilters(); applyFilters(); });
+    row.querySelector('.mf-max').addEventListener('input', e => { fRanges[k].max = e.target.value ? +e.target.value : null; saveMapFilters(); applyFilters(); });
   });
   wrap.querySelector('#mf-reset').addEventListener('click', () => {
     METRICS.forEach(m => fRanges[m.key] = { min: null, max: null });
     wrap.querySelectorAll('input').forEach(i => i.value = '');
-    applyFilters();
+    saveMapFilters(); applyFilters();
+  });
+  // active-filter chips: click ✕ to clear that metric's range
+  wrap.querySelector('#mf-chips').addEventListener('click', e => {
+    const chip = e.target.closest('.mf-chip'); if (!chip) return;
+    const k = chip.dataset.k; fRanges[k] = { min: null, max: null };
+    const row = wrap.querySelector(`.mf-row[data-k="${k}"]`);
+    if (row){ row.querySelector('.mf-min').value = ''; row.querySelector('.mf-max').value = ''; }
+    saveMapFilters(); applyFilters();
   });
+  applyFilters(); // render restored chips/inputs immediately (count fills once metric values load)
 }
 
 function renderLegend(count){
diff --git a/src/ingest/parcels/saltlake_ut.ts b/src/ingest/parcels/saltlake_ut.ts
index 05fc566..dd4fce2 100644
--- a/src/ingest/parcels/saltlake_ut.ts
+++ b/src/ingest/parcels/saltlake_ut.ts
@@ -84,7 +84,7 @@ export async function ingestSaltLake(): Promise<{ upserted: number }> {
           address: norm, norm_address: norm,
           city: s(a.PARCEL_CITY), zip: null,
           lat, lng,
-          year_built: yr(a.BUILT_YR) ?? yr(a.EFFBUILT_YR), sqft: num(a.BLDG_SQFT), beds: null, baths: null,
+          year_built: yr(a.BUILT_YR), sqft: num(a.BLDG_SQFT), beds: null, baths: null, // BUILT_YR only; effective/renovation year lives in extra.eff_year_built
           units: num(a.HOUSE_CNT),
           use_desc: s(a.PROP_CLASS) || s(a.PROP_TYPE),
           land_value: land, improvement_value: (total != null && land != null) ? Math.max(total - land, 0) : null,
@@ -100,6 +100,9 @@ export async function ingestSaltLake(): Promise<{ upserted: number }> {
             note: 'Utah LIR — market value + characteristics; no owner name or sale feed in the public statewide layer',
           }),
         };
+        // skip attribute-less ghost features (ROW/easement polygons UT LIR exports with
+        // no value/address/sqft/use) — coords only, nothing a property lookup can use.
+        if (row.total_value == null && row.address == null && row.sqft == null && row.use_desc == null) continue;
         const prev = batchMap.get(pid);
         if (!prev || (row.total_value ?? 0) > (prev.total_value ?? 0)) batchMap.set(pid, row);
       }

← afd616a auto-save: 2026-07-26T20:15:38 (1 files) — _vpmf.mjs  ·  back to Nationalrealestate  ·  USRE Map: persist county filters to localStorage + active-fi bb39923 →