← 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
M src/lib/places.test.tsM src/lib/places.ts
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 →