[object Object]

← back to Govarbitrage

agents: fix off-topic type gate — exact-token match, not \b-regex

93bf1bd4d041b632e67c3e006e85820b331b4f25 · 2026-07-27 20:23:08 -0700 · Steve Abrams

Google Places types are underscore-joined (furniture_store, shopping_mall).
'_' is a JS word char, so \bstore\b never fired inside a compound type — the
step-1 off-topic gate was a near no-op: an 'Office Furniture Warehouse' tagged
furniture_store (commercial-sounding name) was wrongly KEPT as a CRE broker.

Replace OFF_TOPIC_TYPE_RE / RESIDENTIAL_TYPE_RE with exact-token Sets +
isOffTopicType() (Set membership OR *_store suffix), and expand the blocklist
(shopping_mall, finance, lawyer, car_dealer, *_store, etc). +11 regression tests.
tsc clean; full suite 106/106.

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

Files touched

Diff

commit 93bf1bd4d041b632e67c3e006e85820b331b4f25
Author: Steve Abrams <steve@designerwallcoverings.com>
Date:   Mon Jul 27 20:23:08 2026 -0700

    agents: fix off-topic type gate — exact-token match, not \b-regex
    
    Google Places types are underscore-joined (furniture_store, shopping_mall).
    '_' is a JS word char, so \bstore\b never fired inside a compound type — the
    step-1 off-topic gate was a near no-op: an 'Office Furniture Warehouse' tagged
    furniture_store (commercial-sounding name) was wrongly KEPT as a CRE broker.
    
    Replace OFF_TOPIC_TYPE_RE / RESIDENTIAL_TYPE_RE with exact-token Sets +
    isOffTopicType() (Set membership OR *_store suffix), and expand the blocklist
    (shopping_mall, finance, lawyer, car_dealer, *_store, etc). +11 regression tests.
    tsc clean; full suite 106/106.
    
    Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---
 src/lib/places.test.ts | 34 ++++++++++++++++++++++++++++++++++
 src/lib/places.ts      | 49 +++++++++++++++++++++++++++++++++++++++++--------
 2 files changed, 75 insertions(+), 8 deletions(-)

diff --git a/src/lib/places.test.ts b/src/lib/places.test.ts
index b6a855f..0de4078 100644
--- a/src/lib/places.test.ts
+++ b/src/lib/places.test.ts
@@ -116,6 +116,40 @@ describe("isCommercial — DROP on off-topic Google place types (regardless of n
     // A travel_agency mis-tag on a 'commercial'-named place is still dropped.
     expect(isCommercial(place("Commercial Getaways", ["travel_agency"]))).toBe(false);
   });
+
+  // Regression: Google place types are underscore-joined ("furniture_store"), and
+  // "_" is a JS word char, so the old \bstore\b boundary NEVER fired inside them —
+  // every compound retail/mall type with a commercial-sounding name leaked through
+  // (an "Office Furniture Warehouse" tagged furniture_store was kept as a broker).
+  // Now matched by exact type token + a "*_store" suffix rule.
+  it.each([
+    ["Office Furniture Warehouse", "furniture_store"],
+    ["Commercial Hardware Supply", "hardware_store"],
+    ["Downtown Office Depot", "office_supply_store"],
+    ["Retail Plaza Shopping Mall", "shopping_mall"],
+    ["Prime Electronics Outlet", "electronics_store"],
+    ["Industrial Home Improvement Center", "home_improvement_store"],
+    ["Commercial Auto Group", "car_dealer"],
+    ["Office Legal Advisors", "lawyer"],
+  ])("drops %s (compound off-topic type %s) despite a commercial-sounding name", (name, type) => {
+    expect(isCommercial(place(name, [type]))).toBe(false);
+  });
+
+  it("catches an off-topic type buried in the secondary types array", () => {
+    // primaryType is real_estate_agency but a furniture_store type is also present.
+    expect(
+      isCommercial({
+        id: "x",
+        displayName: { text: "Commercial Furnishings & Realty" },
+        primaryType: "real_estate_agency",
+        types: ["real_estate_agency", "furniture_store"],
+      }),
+    ).toBe(false);
+  });
+
+  it("does NOT over-drop a legit commercial real_estate_agency (no *_store false hit)", () => {
+    expect(isCommercial(place("Commercial Real Estate Brokers", ["real_estate_agency"]))).toBe(true);
+  });
 });
 
 describe("isCommercial — DROP on residential-flavored place types", () => {
diff --git a/src/lib/places.ts b/src/lib/places.ts
index e9b446d..dcf8797 100644
--- a/src/lib/places.ts
+++ b/src/lib/places.ts
@@ -124,12 +124,44 @@ const REAL_ESTATE_TYPES = new Set(["real_estate_agency"]);
 // Off-vertical / off-topic Google place types — drop on match regardless of
 // name. "The Maimon Group" came back as travel_agency; storage/lodging/moving/
 // insurance are common mis-tags that are never a commercial broker.
-const OFF_TOPIC_TYPE_RE =
-  /\b(travel_agency|lodging|insurance_agency|moving_company|storage|self_storage|restaurant|food|store|school|hospital|gym)\b/i;
+//
+// Matched by EXACT type-token equality (not a \b-regex): Google's place types
+// are underscore-joined tokens (e.g. "furniture_store"), and "_" is a JS word
+// character, so a \bstore\b boundary never fires inside "furniture_store" —
+// it silently let every "*_store" / "shopping_mall" / etc. type through. Any
+// type ending in "_store" is also treated as off-topic (Places has dozens of
+// retail *_store subtypes: hardware_store, electronics_store, furniture_store…).
+const OFF_TOPIC_TYPES = new Set([
+  "travel_agency",
+  "lodging",
+  "insurance_agency",
+  "moving_company",
+  "storage",
+  "self_storage",
+  "restaurant",
+  "food",
+  "store",
+  "school",
+  "hospital",
+  "gym",
+  "shopping_mall",
+  "finance", // banks/lenders mis-tagged onto RE queries — not a broker
+  "lawyer",
+  "accounting",
+  "car_dealer",
+  "car_rental",
+  "general_contractor",
+  "home_improvement_store",
+]);
+
+function isOffTopicType(t: string): boolean {
+  const tt = t.toLowerCase();
+  return OFF_TOPIC_TYPES.has(tt) || tt.endsWith("_store");
+}
 
-// Residential-flavored place types (kept from the original; complements the
-// off-topic blocklist for apartment/home-goods mis-tags).
-const RESIDENTIAL_TYPE_RE = /\b(apartment_complex|home_goods_store)\b/i;
+// Residential-flavored place types — exact-token match (same underscore-boundary
+// reason as above). Complements the off-topic blocklist for apartment/home mis-tags.
+const RESIDENTIAL_TYPES = new Set(["apartment_complex", "home_goods_store"]);
 
 function hasCommercialBrand(name: string): boolean {
   const n = name.toLowerCase();
@@ -262,10 +294,11 @@ export function isCommercial(p: RawPlace): boolean {
   const name = p.displayName?.text ?? "";
   const types = [p.primaryType ?? "", ...(p.types ?? [])];
 
-  // 1. Off-vertical place type → drop outright (catches the travel_agency mis-tag).
-  if (types.some((t) => OFF_TOPIC_TYPE_RE.test(t))) return false;
+  // 1. Off-vertical place type → drop outright (catches the travel_agency mis-tag
+  //    AND every underscore-compound retail type like furniture_store/shopping_mall).
+  if (types.some((t) => isOffTopicType(t))) return false;
 
-  const hasResidentialType = types.some((t) => RESIDENTIAL_TYPE_RE.test(t));
+  const hasResidentialType = types.some((t) => RESIDENTIAL_TYPES.has(t.toLowerCase()));
 
   // 2. Positive commercial evidence wins over everything below.
   if (COMMERCIAL_RE.test(name) || hasCommercialBrand(name)) return true;

← 8093362 chore: v0.4.0 → v0.5.0 (session close — basic-auth login + p  ·  back to Govarbitrage  ·  auto-save: 2026-07-27T20:24:23 (1 files) — tsconfig.tsbuildi c173473 →