[object Object]

← back to Dw Photo Capture

Scan fixes: (1) scrub DW's OWN header from identification — our phone 888-373-4564 (+ fragments/phone-shaped tokens) never becomes a candidate code, and 'Designer Wallcoverings' never read as the pattern name; model# is what runs down the SKU. (2) partial search now fires AUTOMATICALLY when a scan doesn't confidently match (uses the OCR'd code via back_read). (3) live search-as-you-type (debounced 300ms + race-guard) in the manual search box

5cea2c8046c2d09b3a7ba89e97c99ee5dcc0ae1b · 2026-07-08 13:04:30 -0700 · Steve Abrams

Files touched

Diff

commit 5cea2c8046c2d09b3a7ba89e97c99ee5dcc0ae1b
Author: Steve Abrams <steve@designerwallcoverings.com>
Date:   Wed Jul 8 13:04:30 2026 -0700

    Scan fixes: (1) scrub DW's OWN header from identification — our phone 888-373-4564 (+ fragments/phone-shaped tokens) never becomes a candidate code, and 'Designer Wallcoverings' never read as the pattern name; model# is what runs down the SKU. (2) partial search now fires AUTOMATICALLY when a scan doesn't confidently match (uses the OCR'd code via back_read). (3) live search-as-you-type (debounced 300ms + race-guard) in the manual search box
---
 public/index.html | 19 +++++++++++++++----
 server.js         | 20 ++++++++++++++++++++
 2 files changed, 35 insertions(+), 4 deletions(-)

diff --git a/public/index.html b/public/index.html
index 40266fc..3ee09a6 100644
--- a/public/index.html
+++ b/public/index.html
@@ -1535,7 +1535,10 @@ async function fbIdentify(){
   $('#fbGo').disabled=true; $('#fbNote').textContent='Identifying…'; $('#fbResult').innerHTML='';
   try{
     const r=await(await fetch('/api/identify-multi',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({back:_fbBack,front:_fbFront,qr:_fbQR})})).json();
-    if(!r.found){ $('#fbNote').textContent='No confident match — try a clearer/closer photo.'; $('#fbResult').innerHTML=fbVisualHtml(r.visual); $('#fbGo').disabled=false; return; }
+    if(!r.found){ const oc=r.code||(r.back_read&&(r.back_read.top||((r.back_read.candidates||[])[0])))||'';
+      if(oc){ $('#fbNote').innerHTML=`No exact match for <b>${oc}</b> — searching the wallpaper file…`; wallpaperSearch(oc); }
+      else { $('#fbNote').textContent='No confident match — try a clearer/closer photo, or type part of the SKU below.'; $('#fbResult').innerHTML=fbVisualHtml(r.visual); }
+      $('#fbGo').disabled=false; return; }
     const p=r.primary, conf=(r.confidence||'').replace('-','');
     const via=r.source==='qr'?'🔗 QR':r.source==='code'?'📄 label code':'🎨 visual';
     $('#fbNote').innerHTML=`Source: ${via}${r.corroborated?' · ✓ front+back agree':''}`;
@@ -1566,10 +1569,13 @@ async function loadWallpaperDetails(p){ const box=$('#fbWpDetails')||$('#fbResul
 }
 // Partial / fuzzy search of the WALLPAPER file by any part of a SKU or description. Renders a candidate
 // list into `box` (defaults to #fbResult for the manual search box).
+let _wpSeq=0;
 async function wallpaperSearch(q, box){ box=box||$('#fbResult'); if(!box) return;
   q=(q||'').trim(); if(q.replace(/[^A-Za-z0-9]/g,'').length<3){ box.innerHTML='<div class="wp-d-hd wp-d-empty">Type at least 3 characters.</div>'; return; }
-  box.innerHTML='<div class="wp-d-hd">Searching the wallpaper file…</div>';
-  let r; try{ r=await(await fetch('/api/wallpaper-search?q='+encodeURIComponent(q))).json(); }catch(e){ box.innerHTML='<div class="wp-d-hd wp-d-empty">Search failed.</div>'; return; }
+  const seq=++_wpSeq;                                              // race guard: only the latest query renders
+  box.innerHTML=`<div class="wp-d-hd">Searching the wallpaper file for “${q}”…</div>`;
+  let r; try{ r=await(await fetch('/api/wallpaper-search?q='+encodeURIComponent(q))).json(); }catch(e){ if(seq===_wpSeq) box.innerHTML='<div class="wp-d-hd wp-d-empty">Search failed.</div>'; return; }
+  if(seq!==_wpSeq) return;                                         // a newer keystroke already superseded this
   const cands=(r&&r.candidates)||[];
   if(!cands.length){ box.innerHTML=`<div class="wp-d-hd wp-d-empty">No wallpaper-file match for “${q}”.</div>`; return; }
   const rowHtml=c=>{ const bits=[c.width&&('W '+c.width),c.repeat&&('R '+c.repeat),c.retail&&('$'+c.retail),c.supplier,c.line].filter(Boolean).join(' · ');
@@ -1621,7 +1627,12 @@ $('#fbBackInput').addEventListener('change',e=>fbRead(e.target.files[0],true));
 $('#fbFrontInput').addEventListener('change',e=>fbRead(e.target.files[0],false));
 $('#fbGo').addEventListener('click',fbIdentify);
 $('#fbSearchBtn').addEventListener('click',()=>wallpaperSearch($('#fbSearchInput').value));
-$('#fbSearchInput').addEventListener('keydown',e=>{ if(e.key==='Enter'){ e.preventDefault(); wallpaperSearch(e.target.value); } });
+$('#fbSearchInput').addEventListener('keydown',e=>{ if(e.key==='Enter'){ e.preventDefault(); clearTimeout(_wpSearchT); wallpaperSearch(e.target.value); } });
+// live search-as-you-type: debounced so we don't fire a FileMaker scan on every keystroke
+let _wpSearchT=null;
+$('#fbSearchInput').addEventListener('input',e=>{ const v=e.target.value; clearTimeout(_wpSearchT);
+  if(v.replace(/[^A-Za-z0-9]/g,'').length<3){ if(!v) $('#fbResult').innerHTML=''; return; }
+  _wpSearchT=setTimeout(()=>wallpaperSearch(v),300); });
 
 // ── Add / Update item: capture up to 10 photos + 6 videos → Shopify (draft-create OR live-update) ──
 const MAX_PHOTOS=10, MAX_VIDEOS=6;
diff --git a/server.js b/server.js
index 9dc0227..d6a8761 100644
--- a/server.js
+++ b/server.js
@@ -353,6 +353,23 @@ const VENDOR_LEX = [
 const STRONG_PFX = /^(GRS-|WD[A-Z]*\d|DWNAT|DW[A-Z]{2})/;   // self-evidently a SKU, no brand needed
 const CODE_TOKEN = /[A-Z0-9][A-Z0-9-]{2,13}/g;             // candidate token shape
 
+// Designer Wallcoverings' OWN header (company name / phone / website) is printed on every DW sample
+// sticker — it is NEVER the product identity. The vendor MODEL # is what runs down the SKU, so scrub
+// our own branding first: our phone (any punctuation form + fragments), phone-shaped tokens, and the
+// DW name/site lines. Configurable via DW_PHONE.
+const DW_PHONE = (process.env.DW_PHONE || '8883734564');                 // 888-373-4564
+const DW_BRAND_RE = /designer\s*wall\s*cover|designerwallcoverings|wallcoverings?\.com|\bDWC\b/i;
+const PHONE_TOKEN_RE = /^(1?[-.\s(]*8(?:00|88|77|66|55)[-.\s)]*\d{3}[-.\s]*\d{4}|\d{10})$/;  // toll-free / 10-digit
+function isDwNoise(tok, dwLabel) {
+  const s = String(tok); const d = s.replace(/\D/g, '');
+  if (!d) return false;
+  if (d === DW_PHONE) return true;                              // our exact phone
+  if (d.length >= 7 && DW_PHONE.includes(d)) return true;       // a 7+ digit fragment of it (373-4564)
+  if (PHONE_TOKEN_RE.test(s)) return true;                      // any 10-digit / toll-free number
+  if (dwLabel && /^\d+$/.test(s) && d.length >= 3 && DW_PHONE.includes(d)) return true;  // phone fragment on OUR label
+  return false;
+}
+
 // ── Field vocabulary: classify a label read into sku# / model# / name / color ──
 // Common interior-design colorway names + basic colors, so a printed color name is caught.
 const COLOR_WORDS = 'oatmeal|alabaster|greige|celadon|ecru|taupe|linen|flax|dove|pewter|charcoal|ebony|onyx|ivory|cream|bone|chalk|parchment|sand|camel|fawn|mushroom|mocha|espresso|chocolate|walnut|honey|amber|ochre|mustard|saffron|terracotta|rust|clay|brick|coral|blush|rose|blossom|mauve|plum|aubergine|burgundy|claret|scarlet|crimson|cardinal|sage|celery|moss|fern|olive|forest|emerald|jade|hunter|teal|aqua|turquoise|peacock|cerulean|cobalt|navy|indigo|denim|slate|sky|powder|periwinkle|lavender|lilac|violet|amethyst|gold|brass|bronze|copper|champagne|platinum|silver|graphite|smoke|stone|pearl|white|black|grey|gray|beige|blue|green|red|yellow|orange|purple|pink|brown|tan';
@@ -386,6 +403,7 @@ function classifyFields(rows, a) {
     name = lines
       .filter(l => /[A-Za-z]/.test(l) && !/\d{3,}/.test(l) && l.replace(/[^A-Za-z]/g, '').length >= 4)
       .filter(l => !(vfirst && l.toUpperCase().includes(vfirst)))
+      .filter(l => !DW_BRAND_RE.test(l))                                   // never read OUR OWN name as the pattern
       .filter(l => !(color && l.toLowerCase() === color.toLowerCase()))   // drop a line that's ONLY the color
       .filter(l => !COLOR_LABEL_RE.test(l) && !/^(colou?r|pattern|design|collection|sku|item|model|mfr)\b/i.test(l))
       .filter(l => !(sku && l.toUpperCase().replace(/\s+/g, '').includes(sku)))
@@ -433,11 +451,13 @@ function analyzeOcr(rows) {
     Math.min(t.length, 12);
   // CODE NUMBERS ONLY — alnum tokens holding a digit (WDW2310, GRS-26220, 5255-16). Plain
   // names and marketing prose are intentionally ignored: scan the code, never the text.
+  const dwLabel = DW_BRAND_RE.test(text) || text.replace(/\D/g, '').includes(DW_PHONE);  // it's OUR sticker
   const seen = new Set(); const codes = [];
   for (const r of rows) {
     if (r.bc) continue;
     for (const t of (r.t.toUpperCase().match(CODE_TOKEN) || [])) {
       if (!/\d/.test(t) || t.length < 4 || seen.has(t)) continue;   // must hold a digit, >=4 chars
+      if (isDwNoise(t, dwLabel)) continue;                          // our phone / phone-shaped → never a SKU
       seen.add(t); codes.push({ t, h: r.h, s: skuScore(t) });
     }
   }

← 8837b5f Scan: partial/fuzzy wallpaper-file lookup — new /api/wallpap  ·  back to Dw Photo Capture  ·  Scan lookup driven by the OCR'd label code: a VISUAL-only id a7f6374 →