fix(ui): use Govmap resolved address text for street inference, not frequency heuristic
Previous approach: pick most common streetNameHeb among street-source deals. Problem: a busier nearby street wins even when the searched parcel is on a quieter street (e.g. רב צעיר beats הילדסהיימר despite being a different street). Fix: backend now returns resolved_address from Govmap autocomplete (e.g. "הילדסהיימר 14, ירושלים"). Frontend parses the leading non-numeric tokens as the street name and uses that as ground truth for proximity labeling. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -165,13 +165,15 @@ def create_app() -> FastAPI:
|
|||||||
logger.exception("find_recent_deals_for_address failed")
|
logger.exception("find_recent_deals_for_address failed")
|
||||||
raise HTTPException(status_code=502, detail=str(e))
|
raise HTTPException(status_code=502, detail=str(e))
|
||||||
|
|
||||||
# Search-center coords for the map (best-effort).
|
# Search-center coords + resolved address text for the UI (best-effort).
|
||||||
search_coords = None
|
search_coords = None
|
||||||
|
resolved_address = None
|
||||||
try:
|
try:
|
||||||
ar = govmap_client.autocomplete_address(req.address)
|
ar = govmap_client.autocomplete_address(req.address)
|
||||||
if ar.results and ar.results[0].coordinates:
|
if ar.results and ar.results[0].coordinates:
|
||||||
c = ar.results[0].coordinates
|
c = ar.results[0].coordinates
|
||||||
search_coords = {"longitude": c.longitude, "latitude": c.latitude}
|
search_coords = {"longitude": c.longitude, "latitude": c.latitude}
|
||||||
|
resolved_address = ar.results[0].text
|
||||||
except Exception:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
@@ -198,6 +200,7 @@ def create_app() -> FastAPI:
|
|||||||
return {
|
return {
|
||||||
"search": {
|
"search": {
|
||||||
"address": req.address,
|
"address": req.address,
|
||||||
|
"resolved_address": resolved_address,
|
||||||
"coordinates": search_coords,
|
"coordinates": search_coords,
|
||||||
"years_back": req.years_back,
|
"years_back": req.years_back,
|
||||||
"radius_meters": req.radius_meters,
|
"radius_meters": req.radius_meters,
|
||||||
|
|||||||
+4
-1
@@ -204,7 +204,10 @@ export default function App() {
|
|||||||
<ErrorBox error={String(deals.error)} />
|
<ErrorBox error={String(deals.error)} />
|
||||||
)}
|
)}
|
||||||
{deals.data && !deals.isFetching && (
|
{deals.data && !deals.isFetching && (
|
||||||
<DealsTable deals={deals.data.deals} />
|
<DealsTable
|
||||||
|
deals={deals.data.deals}
|
||||||
|
resolvedAddress={deals.data.search.resolved_address ?? undefined}
|
||||||
|
/>
|
||||||
)}
|
)}
|
||||||
{!deals.isFetching && !deals.error && !deals.data && (
|
{!deals.isFetching && !deals.error && !deals.data && (
|
||||||
<div className="text-center py-12 text-muted-foreground">
|
<div className="text-center py-12 text-muted-foreground">
|
||||||
|
|||||||
@@ -50,6 +50,7 @@ export interface Deal {
|
|||||||
export interface DealsSearchResponse {
|
export interface DealsSearchResponse {
|
||||||
search: {
|
search: {
|
||||||
address: string;
|
address: string;
|
||||||
|
resolved_address: string | null;
|
||||||
coordinates: Coordinates | null;
|
coordinates: Coordinates | null;
|
||||||
years_back: number;
|
years_back: number;
|
||||||
radius_meters: number;
|
radius_meters: number;
|
||||||
|
|||||||
@@ -3,18 +3,20 @@ import { formatCurrencyILS, formatDate, formatNumber } from "../lib/utils";
|
|||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
deals: Deal[];
|
deals: Deal[];
|
||||||
|
resolvedAddress?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
function inferStreetName(deals: Deal[]): string | null {
|
// Extract street name from a Govmap address string like "הילדסהיימר 14, ירושלים".
|
||||||
const freq: Record<string, number> = {};
|
// Takes all leading tokens that contain no digits.
|
||||||
for (const d of deals) {
|
function streetFromAddress(address: string | undefined): string | null {
|
||||||
if (d.deal_source === "street" && d.streetNameHeb) {
|
if (!address) return null;
|
||||||
freq[d.streetNameHeb] = (freq[d.streetNameHeb] ?? 0) + 1;
|
const tokens = address.split(/[\s,]+/);
|
||||||
|
const parts: string[] = [];
|
||||||
|
for (const t of tokens) {
|
||||||
|
if (/\d/.test(t)) break;
|
||||||
|
if (t) parts.push(t);
|
||||||
}
|
}
|
||||||
}
|
return parts.length ? parts.join(" ") : null;
|
||||||
const entries = Object.entries(freq);
|
|
||||||
if (!entries.length) return null;
|
|
||||||
return entries.reduce((a, b) => (b[1] > a[1] ? b : a))[0];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function proximityLabel(deal: Deal, inferredStreet: string | null): string {
|
function proximityLabel(deal: Deal, inferredStreet: string | null): string {
|
||||||
@@ -34,7 +36,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 }: Props) {
|
export function DealsTable({ deals, resolvedAddress }: 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">
|
||||||
@@ -43,7 +45,7 @@ export function DealsTable({ deals }: Props) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const inferredStreet = inferStreetName(deals);
|
const inferredStreet = streetFromAddress(resolvedAddress);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="overflow-x-auto rounded-md border">
|
<div className="overflow-x-auto rounded-md border">
|
||||||
|
|||||||
Reference in New Issue
Block a user