fix(ui): infer street by closest parcel number in same block

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) <noreply@anthropic.com>
This commit is contained in:
2026-04-25 16:22:59 +00:00
parent 9f305d15b6
commit 291ca59d0b
2 changed files with 37 additions and 3 deletions
+2
View File
@@ -207,6 +207,8 @@ export default function App() {
<DealsTable <DealsTable
deals={deals.data.deals} deals={deals.data.deals}
resolvedAddress={deals.data.search.resolved_address ?? undefined} resolvedAddress={deals.data.search.resolved_address ?? undefined}
searchedBlock={query.block || resolvedBlock}
searchedPlot={query.plot || resolvedPlot}
/> />
)} )}
{!deals.isFetching && !deals.error && !deals.data && ( {!deals.isFetching && !deals.error && !deals.data && (
+35 -3
View File
@@ -4,10 +4,13 @@ import { formatCurrencyILS, formatDate, formatNumber } from "../lib/utils";
interface Props { interface Props {
deals: Deal[]; deals: Deal[];
resolvedAddress?: string; resolvedAddress?: string;
searchedBlock?: string;
searchedPlot?: string;
} }
// Extract street name from a Govmap address string like "הילדסהיימר 14, ירושלים". // 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 { function streetFromAddress(address: string | undefined): string | null {
if (!address) return null; if (!address) return null;
const tokens = address.split(/[\s,]+/); const tokens = address.split(/[\s,]+/);
@@ -19,6 +22,31 @@ function streetFromAddress(address: string | undefined): string | null {
return parts.length ? parts.join(" ") : 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 { function proximityLabel(deal: Deal, inferredStreet: string | null): string {
if (!deal.deal_source) return "—"; if (!deal.deal_source) return "—";
if (deal.deal_source === "same_building") 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"; return "bg-slate-100 text-slate-700";
} }
export function DealsTable({ deals, resolvedAddress }: Props) { export function DealsTable({ deals, resolvedAddress, searchedBlock, searchedPlot }: Props) {
if (!deals.length) { if (!deals.length) {
return ( return (
<div className="text-center py-12 text-muted-foreground"> <div className="text-center py-12 text-muted-foreground">
@@ -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 ( return (
<div className="overflow-x-auto rounded-md border"> <div className="overflow-x-auto rounded-md border">