From 291ca59d0b79273dbb2a4f375a926224b1c03cfd Mon Sep 17 00:00:00 2001 From: Chaim Date: Sat, 25 Apr 2026 16:22:59 +0000 Subject: [PATCH] fix(ui): infer street by closest parcel number in same block MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Govmap autocomplete echoes back "גוש N חלקה M" without resolving to a street, and there's no public reverse-geocoding endpoint. But cadastral parcel numbers are assigned along street frontage, so the deal with parcelNum closest to the searched plot (within the same gush) is almost always on the same street. Use that as the primary signal; fall back to autocomplete text parsing for address-mode searches. Example: search gush 6212 plot 894. Plot 896 (הילדסהיימר 12) is the closest match → "הילדסהיימר" is the searched street → labels work correctly. Co-Authored-By: Claude Opus 4.7 (1M context) --- web/src/App.tsx | 2 ++ web/src/components/DealsTable.tsx | 38 ++++++++++++++++++++++++++++--- 2 files changed, 37 insertions(+), 3 deletions(-) diff --git a/web/src/App.tsx b/web/src/App.tsx index dfda205..71d86bd 100644 --- a/web/src/App.tsx +++ b/web/src/App.tsx @@ -207,6 +207,8 @@ export default function App() { )} {!deals.isFetching && !deals.error && !deals.data && ( diff --git a/web/src/components/DealsTable.tsx b/web/src/components/DealsTable.tsx index b2f42de..9dd5cc4 100644 --- a/web/src/components/DealsTable.tsx +++ b/web/src/components/DealsTable.tsx @@ -4,10 +4,13 @@ import { formatCurrencyILS, formatDate, formatNumber } from "../lib/utils"; interface Props { deals: Deal[]; resolvedAddress?: string; + searchedBlock?: string; + searchedPlot?: string; } // Extract street name from a Govmap address string like "הילדסהיימר 14, ירושלים". -// Takes all leading tokens that contain no digits. +// Takes all leading tokens that contain no digits. Used as fallback only — +// for parcel searches Govmap echoes back "גוש N חלקה M" with no street info. function streetFromAddress(address: string | undefined): string | null { if (!address) return null; const tokens = address.split(/[\s,]+/); @@ -19,6 +22,31 @@ function streetFromAddress(address: string | undefined): string | null { return parts.length ? parts.join(" ") : null; } +// Find the street name of the searched parcel by picking the deal with the +// closest parcelNum within the same gush. Cadastral parcel numbers are usually +// assigned along street frontage, so the nearest plot is almost always the +// same street. +function streetByClosestParcel( + deals: Deal[], + block: string | undefined, + plot: string | undefined +): string | null { + if (!plot) return null; + const target = parseInt(plot, 10); + if (!Number.isFinite(target)) return null; + const blockNum = block ? parseInt(block, 10) : null; + let best: { dist: number; street: string } | null = null; + for (const d of deals) { + if (!d.streetNameHeb || d.parcelNum == null) continue; + if (blockNum !== null && d.gushNum !== blockNum) continue; + const dist = Math.abs((d.parcelNum as number) - target); + if (!best || dist < best.dist) { + best = { dist, street: d.streetNameHeb }; + } + } + return best?.street ?? null; +} + function proximityLabel(deal: Deal, inferredStreet: string | null): string { if (!deal.deal_source) return "—"; if (deal.deal_source === "same_building") return "אותו בניין"; @@ -36,7 +64,7 @@ function proximityClass(deal: Deal, inferredStreet: string | null): string { return "bg-slate-100 text-slate-700"; } -export function DealsTable({ deals, resolvedAddress }: Props) { +export function DealsTable({ deals, resolvedAddress, searchedBlock, searchedPlot }: Props) { if (!deals.length) { return (
@@ -45,7 +73,11 @@ export function DealsTable({ deals, resolvedAddress }: Props) { ); } - const inferredStreet = streetFromAddress(resolvedAddress); + // Prefer closest-parcel inference (works for both address and block_plot + // searches); fall back to parsing the resolved autocomplete text. + const inferredStreet = + streetByClosestParcel(deals, searchedBlock, searchedPlot) ?? + streetFromAddress(resolvedAddress); return (