[object Object]

← back to Homesonspec

Fix broken search: when a query doesn't resolve to a location (city/state/builder name), fall back to a free-text filter (city/community/builder/state) instead of silently returning the entire national catalog

ef19ec03793b0e514f84ab7e230675a4b636b475 · 2026-07-22 22:19:24 -0700 · Steve

Files touched

Diff

commit ef19ec03793b0e514f84ab7e230675a4b636b475
Author: Steve <steve@designerwallcoverings.com>
Date:   Wed Jul 22 22:19:24 2026 -0700

    Fix broken search: when a query doesn't resolve to a location (city/state/builder name), fall back to a free-text filter (city/community/builder/state) instead of silently returning the entire national catalog
---
 packages/search/src/index.ts | 35 ++++++++++++++++++++++++++++++++++-
 1 file changed, 34 insertions(+), 1 deletion(-)

diff --git a/packages/search/src/index.ts b/packages/search/src/index.ts
index d61f5c6..173891d 100644
--- a/packages/search/src/index.ts
+++ b/packages/search/src/index.ts
@@ -13,6 +13,7 @@ import { bboxAround, type BBox } from "@spechomes/shared";
 
 export interface SearchParams {
   q?: string;
+  textQuery?: string; // set when q doesn't resolve to a location — free-text filter fallback
   bbox?: BBox; // explicit map-move bbox skips location resolution
   priceMin?: number;
   priceMax?: number;
@@ -71,6 +72,31 @@ export async function resolveLocation(q: string): Promise<ResolvedLocation | nul
   };
 }
 
+const STATE_NAME_TO_CODE: Record<string, string> = {
+  alabama: "AL", alaska: "AK", arizona: "AZ", arkansas: "AR", california: "CA", colorado: "CO",
+  connecticut: "CT", delaware: "DE", florida: "FL", georgia: "GA", hawaii: "HI", idaho: "ID",
+  illinois: "IL", indiana: "IN", iowa: "IA", kansas: "KS", kentucky: "KY", louisiana: "LA",
+  maine: "ME", maryland: "MD", massachusetts: "MA", michigan: "MI", minnesota: "MN", mississippi: "MS",
+  missouri: "MO", montana: "MT", nebraska: "NE", nevada: "NV", "new hampshire": "NH", "new jersey": "NJ",
+  "new mexico": "NM", "new york": "NY", "north carolina": "NC", "north dakota": "ND", ohio: "OH",
+  oklahoma: "OK", oregon: "OR", pennsylvania: "PA", "rhode island": "RI", "south carolina": "SC",
+  "south dakota": "SD", tennessee: "TN", texas: "TX", utah: "UT", vermont: "VT", virginia: "VA",
+  washington: "WA", "west virginia": "WV", wisconsin: "WI", wyoming: "WY",
+};
+
+/** A query that didn't resolve to a location → match it against city/state/community/builder. */
+function textQueryClause(q: string): Prisma.InventoryHomeWhereInput {
+  const t = q.trim();
+  const stateCode = STATE_NAME_TO_CODE[t.toLowerCase()] ?? (/^[A-Za-z]{2}$/.test(t) ? t.toUpperCase() : null);
+  const or: Prisma.InventoryHomeWhereInput[] = [
+    { city: { contains: t, mode: "insensitive" } },
+    { community: { name: { contains: t, mode: "insensitive" } } },
+    { builder: { name: { contains: t, mode: "insensitive" } } },
+    ...(stateCode ? [{ state: stateCode }] : []),
+  ];
+  return { AND: [{ OR: or }] };
+}
+
 export function buildWhere(params: SearchParams, bbox?: BBox): Prisma.InventoryHomeWhereInput {
   const now = new Date();
   const moveInCutoff =
@@ -82,6 +108,7 @@ export function buildWhere(params: SearchParams, bbox?: BBox): Prisma.InventoryH
     status: "PUBLISHED",
     isDemo: false, // HARD RULE: demonstration/synthetic inventory NEVER surfaces on the live site
     freshness: { not: "INACTIVE" as const }, // Stage-3: deduped/retired records never surface (defense-in-depth over status)
+    ...(params.textQuery ? textQueryClause(params.textQuery) : {}),
     ...(bbox
       ? {
           lat: { gte: bbox.minLat, lte: bbox.maxLat },
@@ -155,6 +182,9 @@ export async function runSearch(params: SearchParams): Promise<SearchResult> {
   if (!bbox && params.q) {
     location = await resolveLocation(params.q);
     bbox = location?.bbox;
+    // Query didn't resolve to a place → fall back to free-text so we never
+    // silently drop the query and return the entire national catalog.
+    if (!bbox) params = { ...params, textQuery: params.q };
   }
 
   const where = buildWhere(params, bbox);
@@ -215,7 +245,10 @@ export async function runSearch(params: SearchParams): Promise<SearchResult> {
  */
 export async function runFacets(params: SearchParams) {
   let bbox = params.bbox;
-  if (!bbox && params.q) bbox = (await resolveLocation(params.q))?.bbox;
+  if (!bbox && params.q) {
+    bbox = (await resolveLocation(params.q))?.bbox;
+    if (!bbox) params = { ...params, textQuery: params.q };
+  }
 
   const without = (key: keyof SearchParams): Prisma.InventoryHomeWhereInput =>
     buildWhere({ ...params, [key]: undefined }, bbox);

← 8231981 Design review fix: dedupe home detail Data-provenance table  ·  back to Homesonspec  ·  Lennar adapter: LENNAR_STATE_FILTER for targeted per-state c c642140 →