diff --git a/docs/validation/2026-08-15-gilmore-heating-air/VALIDATION.md b/docs/validation/2026-08-15-gilmore-heating-air/VALIDATION.md
index 6263e25..04934ea 100644
--- a/docs/validation/2026-08-15-gilmore-heating-air/VALIDATION.md
+++ b/docs/validation/2026-08-15-gilmore-heating-air/VALIDATION.md
@@ -50,7 +50,7 @@
## GATE RESULT
-**PASS** — report_gate.py, 2026-08-15T16:46:21
+**PASS** — report_gate.py, 2026-08-15T16:52:53
- Count: 8 stated / 8 material after review — match
- Identity: report title + date match raw capture ('Gilmore Heating, Air and Plumbing', 15 August 2026) — match
diff --git a/implementation/auditing/multi_scraper.py b/implementation/auditing/multi_scraper.py
index 4cc0cc6..d912f6b 100644
--- a/implementation/auditing/multi_scraper.py
+++ b/implementation/auditing/multi_scraper.py
@@ -130,40 +130,73 @@ async def scrape_google(query, coords, business=""):
return None
+def _phone_from_text(text):
+ """First standalone US phone number in rendered text.
+
+ Lookarounds reject a phone-like substring glued to other digits (the
+ raw-HTML path that previously returned JS artifact numbers).
+ """
+ m = re.search(r"(?, present on JS sites too).
+ Phone is the NAP-critical field: if the direct HTML has no visible phone,
+ re-render via Jina Reader, because business sites usually expose the
+ phone client-side and a false 'no phone' would corrupt the cross-check.
+ """
if not website_url:
return None
import urllib.request
+ html = None
try:
req = urllib.request.Request(website_url, headers={"User-Agent": "Mozilla/5.0"})
with urllib.request.urlopen(req, timeout=10) as resp:
html = resp.read().decode("utf-8", errors="replace")
-
- data = {"source_url": website_url}
-
- # Extract title
- m = re.search(r'
([^<]+)', html, re.IGNORECASE)
+ except Exception as e:
+ print(f" [WARN] Website direct fetch failed ({e}); using Jina Reader")
+ data = {"source_url": website_url}
+ if html:
+ m = re.search(r"([^<]+)", html, re.IGNORECASE)
if m:
data["title"] = m.group(1).strip()
-
- # Extract meta description
m = re.search(r']*name=["\']description["\'][^>]*content=["\']([^"\']+)["\']', html, re.IGNORECASE)
if not m:
m = re.search(r']*content=["\']([^"\']+)["\'][^>]*name=["\']description["\']', html, re.IGNORECASE)
if m:
data["description"] = m.group(1).strip()
-
- # Extract phone from page
- phone_match = re.search(r'(\(?\d{3}\)?[\s-]?\d{3}[\s-]?\d{4})', html)
- if phone_match:
- data["phone_on_page"] = phone_match.group(1)
-
+ data["phone_on_page"] = _phone_from_text(html)
+ if data.get("phone_on_page"):
return data
-
+ # No phone in direct HTML (or fetch failed) -> render via Jina Reader
+ try:
+ md = _jina_markdown(website_url)
except Exception as e:
- print(f" [FAIL] Website: {e}")
- return None
+ print(f" [FAIL] Website (direct + Jina): {e}")
+ return data if html else None
+ if not data.get("title"):
+ m = re.search(r"^Title:\s*(.+)$", md, re.MULTILINE)
+ if m:
+ data["title"] = m.group(1).strip()
+ data["phone_on_page"] = _phone_from_text(md)
+ data["renderer"] = "jina_reader"
+ return data
def merge_and_validate(gbp_data=None, apple_data=None, website_data=None, bing_data=None):
@@ -354,5 +387,19 @@ async def main():
print(f" {key}: {json.dumps(val)}")
+def _selftest():
+ """Runnable check for phone extraction (no network)."""
+ assert _phone_from_text("Call us: (530) 350-9197 today") == "(530) 350-9197"
+ assert _phone_from_text("tel: (530) 350-9197") == "(530) 350-9197"
+ assert _phone_from_text("530-350-9197") == "530-350-9197"
+ # JS artifact: phone-like substring glued to other digits -> reject
+ assert _phone_from_text("var id=1234567890123;") is None
+ assert _phone_from_text("no numbers here") is None
+ print("[SELFTEST] phone extraction: PASS")
+
+
if __name__ == "__main__":
- asyncio.run(main())
+ if "--selftest" in sys.argv:
+ _selftest()
+ else:
+ asyncio.run(main())