← back to Govarbitrage
auto-save: 2026-07-26T17:14:46 (4 files) — src/app/api/agents/search/route.ts src/lib/places.ts src/lib/rate-limit.ts src/lib/places.test.ts
3b38d9148be5c5f2d93d40f8af8a9a720dd1f2cb · 2026-07-26 17:14:52 -0700 · Steve Abrams
Files touched
M src/app/api/agents/search/route.tsA src/lib/places.test.tsM src/lib/places.tsM src/lib/rate-limit.ts
Diff
commit 3b38d9148be5c5f2d93d40f8af8a9a720dd1f2cb
Author: Steve Abrams <steve@designerwallcoverings.com>
Date: Sun Jul 26 17:14:52 2026 -0700
auto-save: 2026-07-26T17:14:46 (4 files) — src/app/api/agents/search/route.ts src/lib/places.ts src/lib/rate-limit.ts src/lib/places.test.ts
---
src/app/api/agents/search/route.ts | 5 +-
src/lib/places.test.ts | 130 +++++++++++++++++++++++++++++++++++++
src/lib/places.ts | 4 +-
src/lib/rate-limit.ts | 11 ++++
4 files changed, 145 insertions(+), 5 deletions(-)
diff --git a/src/app/api/agents/search/route.ts b/src/app/api/agents/search/route.ts
index 59f33d5..5fd8c65 100644
--- a/src/app/api/agents/search/route.ts
+++ b/src/app/api/agents/search/route.ts
@@ -1,6 +1,6 @@
import { NextRequest, NextResponse } from "next/server";
import { searchCommercialAgents, type AgentKind } from "@/lib/places";
-import { rateLimit, clientIp } from "@/lib/rate-limit";
+import { rateLimit, clientIp, sessionOrIpKey } from "@/lib/rate-limit";
import { getCurrentUser } from "@/lib/current-user";
export const dynamic = "force-dynamic";
@@ -12,8 +12,7 @@ export const dynamic = "force-dynamic";
// user is normally present; fall back to the (spoofable) IP only as a last resort.
async function clientKey(req: NextRequest): Promise<string> {
const user = await getCurrentUser();
- if (user?.sub) return "agents:u:" + user.sub;
- return "agents:ip:" + clientIp(req);
+ return sessionOrIpKey("agents", user?.sub, clientIp(req));
}
// Lightweight input validation so user-supplied location can't pad the Places
diff --git a/src/lib/places.test.ts b/src/lib/places.test.ts
new file mode 100644
index 0000000..15c8ab1
--- /dev/null
+++ b/src/lib/places.test.ts
@@ -0,0 +1,130 @@
+import { describe, expect, it } from "vitest";
+import { isCommercial, type RawPlace } from "./places";
+
+// isCommercial() is the finder's core value prop: precision-first
+// commercial-vs-residential classification (the LA 57→28 verified drop).
+// These offline tests lock that behavior in — zero live Google Places billing,
+// per project rails. The apply order under test (see places.ts):
+// 1. off-topic Google type → DROP
+// 2. strong commercial name OR allowlisted brand → KEEP (beats residential)
+// 3. residential/irrelevant name → DROP
+// 4. residential-flavored type → DROP
+// 5/6. no commercial evidence → DROP (precision > recall)
+
+function place(name: string, types: string[] = ["real_estate_agency"]): RawPlace {
+ return { id: "x", displayName: { text: name }, primaryType: types[0], types };
+}
+
+describe("isCommercial — KEEP on strong commercial name signals", () => {
+ it.each([
+ "Downtown Commercial Real Estate Brokers",
+ "Metro CRE Advisors",
+ "Industrial Property Group",
+ "Prime Office Leasing",
+ "Retail & Industrial Brokerage",
+ "Apex Investment Sales",
+ "Tenant Representation Partners",
+ "Net Lease Advisors",
+ "Summit Capital Markets",
+ "Bay Area Multifamily Group",
+ "Harbor Income Properties",
+ ])("keeps %s", (name) => {
+ expect(isCommercial(place(name))).toBe(true);
+ });
+});
+
+describe("isCommercial — KEEP on allowlisted national brokerage brands", () => {
+ it.each([
+ "CBRE",
+ "JLL Los Angeles",
+ "Cushman & Wakefield",
+ "Colliers International",
+ "Marcus & Millichap",
+ "Lee & Associates",
+ "Newmark Group",
+ "Avison Young",
+ "Hughes Marino",
+ "NAI Capital",
+ ])("keeps brand %s", (name) => {
+ expect(isCommercial(place(name))).toBe(true);
+ });
+
+ it("is case-insensitive on brand match", () => {
+ expect(isCommercial(place("cbre commercial"))).toBe(true);
+ expect(isCommercial(place("MARCUS AND MILLICHAP"))).toBe(true);
+ });
+});
+
+describe("isCommercial — commercial evidence WINS over a residential word", () => {
+ it("keeps a name carrying both commercial and residential signals", () => {
+ // "commercial" (KEEP, step 2) is evaluated before "homes" (DROP, step 3).
+ expect(isCommercial(place("Commercial & Home Sales Group"))).toBe(true);
+ });
+ it("keeps an allowlisted brand even next to a residential word", () => {
+ expect(isCommercial(place("CBRE Residential Division"))).toBe(true);
+ });
+});
+
+describe("isCommercial — DROP on residential / irrelevant names", () => {
+ it.each([
+ "John Smith, Realtor",
+ "Dream Homes Realty",
+ "Sunset Home Sales",
+ "First-Time Buyer Specialists",
+ "The Home Team",
+ "Bluewater Property Management",
+ "City Apartments Locators",
+ "Acme Mortgage & Home Loans",
+ "Luxury Condos of Malibu",
+ "Single Family Realty Team",
+ ])("drops %s", (name) => {
+ expect(isCommercial(place(name))).toBe(false);
+ });
+});
+
+describe("isCommercial — DROP on generic / no-evidence real-estate results", () => {
+ it("drops a plain real_estate_agency with no commercial signal", () => {
+ // Precision-first: Google's real_estate_agency type can't confirm commercial.
+ expect(isCommercial(place("Smith Real Estate"))).toBe(false);
+ expect(isCommercial(place("Downtown Realty"))).toBe(false);
+ });
+ it("drops a non-real-estate result with no signal either way", () => {
+ expect(isCommercial(place("Bob's Consulting", ["consultant"]))).toBe(false);
+ });
+});
+
+describe("isCommercial — DROP on off-topic Google place types (regardless of name)", () => {
+ it.each([
+ ["The Maimon Group", "travel_agency"],
+ ["Cozy Stays", "lodging"],
+ ["Shield Insurance", "insurance_agency"],
+ ["Two Guys Moving", "moving_company"],
+ ["SecureBox Self Storage", "self_storage"],
+ ["Mario's Restaurant", "restaurant"],
+ ])("drops %s tagged %s", (name, type) => {
+ expect(isCommercial(place(name, [type]))).toBe(false);
+ });
+
+ it("off-topic type beats an otherwise-commercial name", () => {
+ // A travel_agency mis-tag on a 'commercial'-named place is still dropped.
+ expect(isCommercial(place("Commercial Getaways", ["travel_agency"]))).toBe(false);
+ });
+});
+
+describe("isCommercial — DROP on residential-flavored place types", () => {
+ it.each(["apartment_complex", "home_goods_store"])("drops type %s with no commercial name", (type) => {
+ expect(isCommercial(place("Parkview", [type]))).toBe(false);
+ });
+ it("commercial name still wins over a residential-flavored type", () => {
+ expect(isCommercial(place("Commercial Leasing at Parkview", ["apartment_complex"]))).toBe(true);
+ });
+});
+
+describe("isCommercial — edge cases", () => {
+ it("drops a place with no name", () => {
+ expect(isCommercial({ id: "x", displayName: { text: "" }, types: ["real_estate_agency"] })).toBe(false);
+ });
+ it("drops a place with no types and no commercial name", () => {
+ expect(isCommercial({ id: "x", displayName: { text: "Generic Agency" } })).toBe(false);
+ });
+});
diff --git a/src/lib/places.ts b/src/lib/places.ts
index df8f778..b1d81fb 100644
--- a/src/lib/places.ts
+++ b/src/lib/places.ts
@@ -136,7 +136,7 @@ function hasCommercialBrand(name: string): boolean {
return COMMERCIAL_BRANDS.some((b) => n.includes(b));
}
-interface RawPlace {
+export interface RawPlace {
id?: string;
displayName?: { text?: string };
formattedAddress?: string;
@@ -166,7 +166,7 @@ function locationString(city?: string, state?: string, zip?: string): string {
// 4. Residential-flavored place type → DROP.
// 5. Generic real-estate result with no commercial evidence → DROP.
// 6. Non-real-estate result with no signal either way → DROP (nothing kept it).
-function isCommercial(p: RawPlace): boolean {
+export function isCommercial(p: RawPlace): boolean {
const name = p.displayName?.text ?? "";
const types = [p.primaryType ?? "", ...(p.types ?? [])];
diff --git a/src/lib/rate-limit.ts b/src/lib/rate-limit.ts
index 7153f30..c148844 100644
--- a/src/lib/rate-limit.ts
+++ b/src/lib/rate-limit.ts
@@ -50,6 +50,17 @@ export function clientIp(req: NextRequest): string {
return req.headers.get("x-real-ip") || "unknown";
}
+/**
+ * Build a rate-limit bucket key that prefers the UNFORGEABLE session subject
+ * over the spoofable client IP. A logged-in user is keyed on their verified
+ * `sub` so they can't reset their bucket (and run up the paid Places bill) by
+ * rotating X-Forwarded-For; only anonymous/last-resort callers fall back to IP.
+ * Pure + exported so the security invariant is unit-testable.
+ */
+export function sessionOrIpKey(prefix: string, userSub: string | null | undefined, ip: string): string {
+ return userSub ? `${prefix}:u:${userSub}` : `${prefix}:ip:${ip}`;
+}
+
/** 429 response with a Retry-After header. */
export function tooManyRequests(r: RateResult, extraHeaders?: Record<string, string>): NextResponse {
return NextResponse.json(
← 8b39674 agents: harden search route — session-keyed rate-limit (unsp
·
back to Govarbitrage
·
agents: contact-based dedupe — collapse same office surfaced b791b34 →