[object Object]

← back to Rentv Ad Engine

Phase 2b: advertiser ADDRESS field via free website lookup (regex + local-LLM fallback) — deep in-video frames blocked by Vimeo DRM + addresses are read verbally not shown; UI shows address (maps link) + needs-web-search flag

e9257f66fae570e14e375b1e63d867f377e701eb · 2026-08-07 09:27:12 -0700 · Steve Abrams

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

Files touched

Diff

commit e9257f66fae570e14e375b1e63d867f377e701eb
Author: Steve Abrams <steve@designerwallcoverings.com>
Date:   Fri Aug 7 09:27:12 2026 -0700

    Phase 2b: advertiser ADDRESS field via free website lookup (regex + local-LLM fallback) — deep in-video frames blocked by Vimeo DRM + addresses are read verbally not shown; UI shows address (maps link) + needs-web-search flag
    
    Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---
 data/advertisers.json       | 103 ++++++++++++++++++++++++++++------------
 public/index.html           |   3 ++
 scripts/enrich-addresses.py | 111 ++++++++++++++++++++++++++++++++++++++++++++
 3 files changed, 187 insertions(+), 30 deletions(-)

diff --git a/data/advertisers.json b/data/advertisers.json
index 2c2fd16..ddbdfe2 100644
--- a/data/advertisers.json
+++ b/data/advertisers.json
@@ -1,5 +1,5 @@
 {
- "generated_at": "2026-08-07 08:55:23",
+ "generated_at": "2026-08-07 09:26:24",
  "count": 29,
  "advertisers": [
   {
@@ -115,7 +115,8 @@
      "title": "RENTV CRE Talk Episode #1 - Bill Shopoff",
      "url": "https://vimeo.com/771348410"
     }
-   ]
+   ],
+   "address_status": "needs_web_search"
   },
   {
    "name": "Fidelity Mortgage Lenders",
@@ -217,7 +218,8 @@
      "title": "RENTV FINANCE OCT 19 2022.mp4",
      "url": "https://vimeo.com/763447786"
     }
-   ]
+   ],
+   "address_status": "needs_web_search"
   },
   {
    "name": "Paramount Property Tax Appeal",
@@ -295,7 +297,8 @@
      "title": "RENTV CRE Talk Episode #1 - Bill Shopoff",
      "url": "https://vimeo.com/771348410"
     }
-   ]
+   ],
+   "address_status": "needs_web_search"
   },
   {
    "name": "Chase Partners",
@@ -364,7 +367,11 @@
      "title": "RENTV CRE Talk Episode #14 - John McNellis",
      "url": "https://vimeo.com/1056513923"
     }
-   ]
+   ],
+   "addresses": [
+    "Chase Partners Ltd., P.O Box 3944, Glendale, CA 91221"
+   ],
+   "address_status": "found"
   },
   {
    "name": "Redwood Mortgage",
@@ -414,7 +421,11 @@
      "title": "RENTV CRE Talk Episode #1 - Bill Shopoff",
      "url": "https://vimeo.com/771348410"
     }
-   ]
+   ],
+   "addresses": [
+    "155 Bovet Road, Suite 302 San Mateo, CA 94402"
+   ],
+   "address_status": "found"
   },
   {
    "name": "Provident Savings Bank",
@@ -450,7 +461,11 @@
      "title": "CRE Talk Episode #20 with Colton Sudberry",
      "url": "https://vimeo.com/1178561985"
     }
-   ]
+   ],
+   "addresses": [
+    "1891 S. Santa Fe Ave., San Jacinto, CA 92581"
+   ],
+   "address_status": "found"
   },
   {
    "name": "Rockefeller Group",
@@ -480,7 +495,13 @@
      "title": "CRE Talk Episode #20 with Colton Sudberry",
      "url": "https://vimeo.com/1178561985"
     }
-   ]
+   ],
+   "addresses": [
+    "2000 Our Offices Arizona 2325 East Camelback Road Suite 400 Phoenix, AZ 85016",
+    "5082 California 3161 Michelson Drive Suite 900 Irvine, CA 92612",
+    "1800 Colorado 3400 Walnut Street Suite 240 Denver, CO 80205"
+   ],
+   "address_status": "found"
   },
   {
    "name": "The Bascom Group",
@@ -503,7 +524,8 @@
      "title": "RENTV ORANGE COUNTY MULTIFAMILY PANEL July 26, 2022",
      "url": "https://vimeo.com/739081927"
     }
-   ]
+   ],
+   "address_status": "needs_web_search"
   },
   {
    "name": "Advanced Real Estate Services",
@@ -521,7 +543,8 @@
      "title": "The Multifamily Panel at RENTV's Greater LA State of the Market Conference",
      "url": "https://vimeo.com/865035925"
     }
-   ]
+   ],
+   "address_status": "needs_web_search"
   },
   {
    "name": "Advanced Real Estate The Firm",
@@ -539,7 +562,8 @@
      "title": "RENTV ORANGE COUNTY MULTIFAMILY PANEL July 26, 2022",
      "url": "https://vimeo.com/739081927"
     }
-   ]
+   ],
+   "address_status": "needs_web_search"
   },
   {
    "name": "Axo Bank",
@@ -557,7 +581,8 @@
      "title": "The Finance Panel at RENTV's Greater LA State of the Market Conference",
      "url": "https://vimeo.com/862593366"
     }
-   ]
+   ],
+   "address_status": "needs_web_search"
   },
   {
    "name": "Axos Bank",
@@ -575,7 +600,8 @@
      "title": "RENTV FINANCE OCT 19 2022.mp4",
      "url": "https://vimeo.com/763447786"
     }
-   ]
+   ],
+   "address_status": "needs_web_search"
   },
   {
    "name": "CBRE",
@@ -593,7 +619,8 @@
      "title": "RENTV ORANGE COUNTY MULTIFAMILY PANEL July 26, 2022",
      "url": "https://vimeo.com/739081927"
     }
-   ]
+   ],
+   "address_status": "needs_web_search"
   },
   {
    "name": "CBRE's National Retail Partners West Team",
@@ -613,7 +640,8 @@
      "title": "The Retail Panel at RENTV's Inland Empire State of The Market Conference",
      "url": "https://vimeo.com/830276773"
     }
-   ]
+   ],
+   "address_status": "needs_web_search"
   },
   {
    "name": "City of Ontario",
@@ -633,7 +661,8 @@
      "title": "The Retail Panel at RENTV's Inland Empire State of The Market Conference",
      "url": "https://vimeo.com/830276773"
     }
-   ]
+   ],
+   "address_status": "needs_web_search"
   },
   {
    "name": "First",
@@ -651,7 +680,8 @@
      "title": "RENTV CRE Talk Episode #13 - Ethan Penner",
      "url": "https://vimeo.com/1039101858"
     }
-   ]
+   ],
+   "address_status": "needs_web_search"
   },
   {
    "name": "Jones Lang LaSalle (JLL)",
@@ -671,7 +701,8 @@
      "title": "The Office Panel at RENTV's Inland Empire State of The Market Conference",
      "url": "https://vimeo.com/830279080"
     }
-   ]
+   ],
+   "address_status": "needs_web_search"
   },
   {
    "name": "MG R Real Estate",
@@ -689,7 +720,8 @@
      "title": "The Office Panel at RENTV's Inland Empire State of The Market Conference",
      "url": "https://vimeo.com/830279080"
     }
-   ]
+   ],
+   "address_status": "needs_web_search"
   },
   {
    "name": "MGR Real Estate Services",
@@ -707,7 +739,8 @@
      "title": "RENTV CRE Talk Episode #11 - Michael Rademaker",
      "url": "https://vimeo.com/953281926"
     }
-   ]
+   ],
+   "address_status": "needs_web_search"
   },
   {
    "name": "Ms. Rahi TE Bank",
@@ -725,7 +758,8 @@
      "title": "The Finance Panel at RENTV's Greater LA State of the Market Conference",
      "url": "https://vimeo.com/862593366"
     }
-   ]
+   ],
+   "address_status": "needs_web_search"
   },
   {
    "name": "Newmark Merrill Companies",
@@ -745,7 +779,8 @@
      "title": "The Retail Panel at RENTV's Inland Empire State of The Market Conference",
      "url": "https://vimeo.com/830276773"
     }
-   ]
+   ],
+   "address_status": "needs_web_search"
   },
   {
    "name": "Pacific Southwest Realty Services",
@@ -763,7 +798,8 @@
      "title": "The Finance Panel at RENTV's Greater LA State of the Market Conference",
      "url": "https://vimeo.com/862593366"
     }
-   ]
+   ],
+   "address_status": "needs_web_search"
   },
   {
    "name": "Paragon Commercial Group",
@@ -783,7 +819,8 @@
      "title": "The Retail Panel at RENTV's Inland Empire State of The Market Conference",
      "url": "https://vimeo.com/830276773"
     }
-   ]
+   ],
+   "address_status": "needs_web_search"
   },
   {
    "name": "Provident Bank",
@@ -801,7 +838,8 @@
      "title": "The Finance Panel at RENTV's Greater LA State of the Market Conference",
      "url": "https://vimeo.com/862593366"
     }
-   ]
+   ],
+   "address_status": "needs_web_search"
   },
   {
    "name": "rent TV",
@@ -819,7 +857,8 @@
      "title": "CLIC Vlog w Steve Bloom.mov",
      "url": "https://vimeo.com/721184140"
     }
-   ]
+   ],
+   "address_status": "needs_web_search"
   },
   {
    "name": "Say What's Properties",
@@ -837,7 +876,8 @@
      "title": "RENTV ORANGE COUNTY MULTIFAMILY PANEL July 26, 2022",
      "url": "https://vimeo.com/739081927"
     }
-   ]
+   ],
+   "address_status": "needs_web_search"
   },
   {
    "name": "Torrey Pines Bank",
@@ -855,7 +895,8 @@
      "title": "RENTV FINANCE OCT 19 2022.mp4",
      "url": "https://vimeo.com/763447786"
     }
-   ]
+   ],
+   "address_status": "needs_web_search"
   },
   {
    "name": "Universe Holdings",
@@ -873,7 +914,8 @@
      "title": "The Multifamily Panel at RENTV's Greater LA State of the Market Conference",
      "url": "https://vimeo.com/865035925"
     }
-   ]
+   ],
+   "address_status": "needs_web_search"
   },
   {
    "name": "Walker and Dunlop",
@@ -891,7 +933,8 @@
      "title": "RENTV FINANCE OCT 19 2022.mp4",
      "url": "https://vimeo.com/763447786"
     }
-   ]
+   ],
+   "address_status": "needs_web_search"
   }
  ]
 }
\ No newline at end of file
diff --git a/public/index.html b/public/index.html
index 7d664a6..ab7910b 100644
--- a/public/index.html
+++ b/public/index.html
@@ -183,6 +183,7 @@ function renderCards(main,L){
       ${(a.websites||[]).map(w=>`<div class="kv">🌐 <a href="${siteURL(esc(w))}" target="_blank" rel="noopener">${esc(w)}</a></div>`).join("")}
       ${(a.phones||[]).map(p=>`<div class="kv">📞 <a href="${telURL(p)}">${esc(p)}</a></div>`).join("")}
       ${(a.emails||[]).map(m=>`<div class="kv">✉️ <a href="mailto:${esc(m)}">${esc(m)}</a></div>`).join("")}
+      ${(a.addresses||[]).slice(0,1).map(ad=>`<div class="kv">📍 <a href="https://maps.google.com/?q=${encodeURIComponent(ad)}" target="_blank" rel="noopener">${esc(ad)}</a></div>`).join("")}
       ${(a.contacts||[]).length?`<div class="kv">👤 <b>${esc(a.contacts.join(", "))}</b></div>`:""}
       <div class="row">${epChips(a.episodes)}</div>`;
     g.appendChild(c);
@@ -303,6 +304,8 @@ window.openAdv=name=>{
     ${(a.websites||[]).map(w=>`<div>🌐 <a href="${siteURL(esc(w))}" target="_blank" rel="noopener">${esc(w)}</a></div>`).join("")}
     ${(a.phones||[]).map(p=>`<div>📞 <a href="${telURL(p)}">${esc(p)}</a></div>`).join("")}
     ${(a.emails||[]).map(m=>`<div>✉️ <a href="mailto:${esc(m)}">${esc(m)}</a></div>`).join("")}
+    ${(a.addresses||[]).map(ad=>`<div>📍 <a href="https://maps.google.com/?q=${encodeURIComponent(ad)}" target="_blank" rel="noopener">${esc(ad)}</a></div>`).join("")}
+    ${a.address_status==="needs_web_search"?'<div class="muted">📍 address: needs web lookup (no website captured)</div>':""}
     ${(a.contacts||[]).length?`<div>👤 ${esc(a.contacts.join(", "))}</div>`:""}
     <div style="margin:12px 0"><button class="mailbtn" onclick='genEmail("advertiser",${JSON.stringify(a.name)},0)'>✉️ Generate email to client</button></div>
     <h3 style="margin-top:8px">Ran on these episodes</h3>
diff --git a/scripts/enrich-addresses.py b/scripts/enrich-addresses.py
new file mode 100644
index 0000000..8f768fc
--- /dev/null
+++ b/scripts/enrich-addresses.py
@@ -0,0 +1,111 @@
+#!/usr/bin/env python3
+"""Phase 2 (b) — fill the ADVERTISER ADDRESS field via free web lookup.
+
+Deep in-video frame OCR was blocked by Vimeo's stream DRM AND the addresses are read
+verbally, not shown on screen — so the reliable source of an advertiser's mailing
+address is their OWN website. For each advertiser that has a website, we fetch the
+homepage + common contact pages, extract the best US street address (regex, with a
+local-LLM cleanup on ambiguous cases), and write it into advertisers.json.
+
+Advertisers with NO website captured are flagged needs_web_search (a metered lookup,
+left for a Steve-gated pass). Local + free ($0); writes addresses[] onto each record.
+
+Env: OLLAMA_URL, OLLAMA_MODEL (default qwen2.5:latest), FORCE=1
+"""
+import json, os, re, urllib.request, time
+
+ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
+ADV = os.path.join(ROOT, "data", "advertisers.json")
+OLLAMA = os.environ.get("OLLAMA_URL", "http://127.0.0.1:11434")
+MODEL = os.environ.get("OLLAMA_MODEL", "qwen2.5:latest")
+FORCE = os.environ.get("FORCE", "") == "1"
+UA = "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 Safari/605.1"
+
+ADDR_RE = re.compile(
+    r"\d{1,6}\s+[A-Z][A-Za-z0-9.'&/ ]{2,40}"
+    r"(?:Street|St\.?|Avenue|Ave\.?|Boulevard|Blvd\.?|Road|Rd\.?|Drive|Dr\.?|Lane|Ln\.?|"
+    r"Way|Plaza|Center|Centre|Court|Ct\.?|Parkway|Pkwy\.?|Circle|Highway|Hwy\.?)"
+    r"[A-Za-z0-9.,'# ]{0,40}?"
+    r"(?:Suite|Ste\.?|Floor|Fl\.?|#|Unit)?[A-Za-z0-9.,'# ]{0,20}?"
+    r",?\s+[A-Z][A-Za-z ]{2,20},?\s+[A-Z]{2}\s+\d{5}")
+
+
+def fetch(url):
+    try:
+        return urllib.request.urlopen(urllib.request.Request(url, headers={"User-Agent": UA}), timeout=18)\
+            .read().decode("utf-8", "ignore")
+    except Exception:
+        return ""
+
+
+def page_text(html):
+    t = re.sub(r"<(script|style)[^>]*>.*?</\1>", " ", html, flags=re.S | re.I)
+    t = re.sub(r"<[^>]+>", " ", t)
+    import html as _h
+    return re.sub(r"\s+", " ", _h.unescape(t))
+
+
+def best_address(text):
+    cands = []
+    for m in ADDR_RE.finditer(text):
+        s = re.sub(r"\s+", " ", m.group(0)).strip(" ,")
+        if 12 <= len(s) <= 100 and s not in cands:
+            cands.append(s)
+    return cands[:3]
+
+
+def llm_pick(name, blob):
+    """Ambiguous case: ask local model for the company's HQ mailing address from page text."""
+    prompt = (f'From this website text for "{name}", extract the company mailing address if present. '
+              'Return ONLY JSON: {"address":"full street address incl city, state, ZIP, or empty if none"}\n\n'
+              + blob[:4000])
+    try:
+        body = json.dumps({"model": MODEL, "prompt": prompt, "format": "json", "stream": False,
+                           "options": {"temperature": 0}}).encode()
+        raw = json.load(urllib.request.urlopen(urllib.request.Request(
+            OLLAMA + "/api/generate", data=body, headers={"Content-Type": "application/json"}), timeout=120)
+        ).get("response", "")
+        a = json.loads(raw).get("address", "").strip()
+        return a if a and re.search(r"\d{5}", a) else ""
+    except Exception:
+        return ""
+
+
+def main():
+    doc = json.load(open(ADV))
+    advs = doc["advertisers"]
+    done = need = 0
+    for a in advs:
+        if a.get("addresses") and not FORCE:
+            continue
+        site = a["websites"][0] if a.get("websites") else ""
+        if not site:
+            a["address_status"] = "needs_web_search"   # metered, Steve-gated
+            need += 1
+            continue
+        base = "https://" + site
+        found = []
+        for path in ("", "/contact", "/contact-us", "/about", "/contact.html"):
+            txt = page_text(fetch(base + path))
+            if txt:
+                found = best_address(txt)
+                if found:
+                    break
+        if not found:
+            # LLM fallback on the contact page text
+            txt = page_text(fetch(base + "/contact") or fetch(base))
+            g = llm_pick(a["name"], txt)
+            if g:
+                found = [g]
+        a["addresses"] = found
+        a["address_status"] = "found" if found else "not_on_site"
+        done += 1 if found else 0
+        print(f"  {a['name'][:34]:34} -> {found[0] if found else '(none on site)'}")
+        time.sleep(0.3)
+    doc["generated_at"] = time.strftime("%Y-%m-%d %H:%M:%S")
+    json.dump(doc, open(ADV, "w"), indent=1)
+    print(f"[addresses] DONE  found={done}  needs_web_search={need}  -> advertisers.json")
+
+
+if __name__ == "__main__":
+    main()

← 8ae82f6 Emails tab: batch-drafted client emails for all 29 advertise  ·  back to Rentv Ad Engine  ·  Phase 2b: web-search address enrichment — 24/29 advertisers 6eeb40a →