feat(appraisals): tiered nearby search; center-align numeric columns
UI:
- Center-align all numeric table columns (deals + appraisals) for better
visual balance — text-start was crowding numbers against the right edge.
Backend (search_appraisals):
- Add include_nearby + nearby_block_radius flags. When set, fetches three
buckets in one request:
1. exact match (block + plot, original behavior)
2. same block, other plots
3. nearby blocks (block ± radius, default ±2)
- Composite-key dedupe across buckets (gov.il has no stable decision id).
Frontend:
- AppraisalsTable now renders the three buckets as stacked sections with
Hebrew headers ("החלטות תואמות" / "באותו גוש" / "בגושים סמוכים").
- Tab counter sums all three buckets.
Auto-enables include_nearby whenever a block is in scope (address mode
via gush_helka lookup, or block_plot mode directly).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
+100
-17
@@ -59,6 +59,11 @@ class AppraisalsSearchRequest(BaseModel):
|
||||
search_text: str | None = Field(None, max_length=200)
|
||||
appraisal_header: str | None = Field(None, max_length=200)
|
||||
max_results: int = Field(30, ge=1, le=100)
|
||||
# When true and `block` is supplied, also fetch decisions from other
|
||||
# plots in the same block and from neighbouring blocks. Returned in
|
||||
# separate buckets in the response.
|
||||
include_nearby: bool = Field(False)
|
||||
nearby_block_radius: int = Field(2, ge=1, le=10)
|
||||
|
||||
|
||||
# ──────────────────────────────────────────────────────────────────────
|
||||
@@ -213,12 +218,19 @@ def create_app() -> FastAPI:
|
||||
# ── /api/search/appraisals ────────────────────────────────────────
|
||||
@app.post("/api/search/appraisals")
|
||||
async def search_appraisals(req: AppraisalsSearchRequest):
|
||||
"""Search decisive-appraiser decisions (gov.il / Ministry of Justice)."""
|
||||
try:
|
||||
response = decisive_appraiser_client.search_decisions_paged(
|
||||
"""Search decisive-appraiser decisions (gov.il / Ministry of Justice).
|
||||
|
||||
If `include_nearby=True` and a block is provided, also runs follow-up
|
||||
searches for the same block (different plots) and for adjacent blocks,
|
||||
returned in separate buckets. The buckets are mutually exclusive
|
||||
(deduped by upstream decision id).
|
||||
"""
|
||||
|
||||
def _run(block: str | None, plot: str | None) -> Any:
|
||||
return decisive_appraiser_client.search_decisions_paged(
|
||||
max_results=req.max_results,
|
||||
block=req.block,
|
||||
plot=req.plot,
|
||||
block=block,
|
||||
plot=plot,
|
||||
decisive_appraiser=req.decisive_appraiser,
|
||||
committee=req.committee,
|
||||
decision_date_from=req.decision_date_from,
|
||||
@@ -228,24 +240,95 @@ def create_app() -> FastAPI:
|
||||
search_text=req.search_text,
|
||||
appraisal_header=req.appraisal_header,
|
||||
)
|
||||
|
||||
try:
|
||||
exact_response = _run(req.block, req.plot)
|
||||
except Exception as e:
|
||||
logger.exception("search_decisions_paged failed")
|
||||
logger.exception("search_decisions_paged (exact) failed")
|
||||
raise HTTPException(status_code=502, detail=str(e))
|
||||
|
||||
decisions = []
|
||||
for idx, decision in enumerate(response.results, start=1):
|
||||
d = decision.model_dump(mode="json", exclude_none=True)
|
||||
documents = d.pop("documents", []) or []
|
||||
d["id"] = idx
|
||||
d["pdf_url"] = documents[0]["file_url"] if documents else None
|
||||
d["all_documents"] = documents
|
||||
decisions.append(d)
|
||||
# Dedupe across buckets by a composite key (gov.il doesn't expose a
|
||||
# single stable id we can rely on).
|
||||
seen_keys: set[tuple] = set()
|
||||
|
||||
def _serialize(decisions_iter: Any, start_idx: int = 1) -> list[dict]:
|
||||
out: list[dict] = []
|
||||
running = start_idx
|
||||
for decision in decisions_iter:
|
||||
key = (
|
||||
decision.appraisal_header,
|
||||
decision.decision_date,
|
||||
decision.decisive_appraiser,
|
||||
decision.block,
|
||||
decision.plot,
|
||||
)
|
||||
if key in seen_keys:
|
||||
continue
|
||||
seen_keys.add(key)
|
||||
d = decision.model_dump(mode="json", exclude_none=True)
|
||||
documents = d.pop("documents", []) or []
|
||||
d["id"] = running
|
||||
d["pdf_url"] = documents[0]["file_url"] if documents else None
|
||||
d["all_documents"] = documents
|
||||
out.append(d)
|
||||
running += 1
|
||||
return out
|
||||
|
||||
exact_decisions = _serialize(exact_response.results)
|
||||
|
||||
same_block_decisions: list[dict] = []
|
||||
nearby_block_decisions: list[dict] = []
|
||||
|
||||
if req.include_nearby and req.block:
|
||||
# Same block, different plots — only meaningful if a plot was given;
|
||||
# otherwise the exact search already covers the whole block.
|
||||
if req.plot:
|
||||
try:
|
||||
sb = _run(req.block, None)
|
||||
same_block_decisions = _serialize(
|
||||
sb.results, start_idx=len(exact_decisions) + 1
|
||||
)
|
||||
except Exception:
|
||||
logger.exception("same-block appraisals lookup failed")
|
||||
|
||||
# Neighbouring blocks (numeric ±radius). Israeli cadastral blocks
|
||||
# are usually sequential within a city, so this is a reasonable
|
||||
# heuristic for "nearby parcels" without needing a spatial query.
|
||||
try:
|
||||
block_int = int(req.block)
|
||||
except ValueError:
|
||||
block_int = None
|
||||
|
||||
if block_int is not None:
|
||||
base_idx = len(exact_decisions) + len(same_block_decisions) + 1
|
||||
deltas = list(range(1, req.nearby_block_radius + 1))
|
||||
ordered_neighbours = []
|
||||
for delta in deltas:
|
||||
ordered_neighbours.extend([block_int - delta, block_int + delta])
|
||||
for nb in ordered_neighbours:
|
||||
if nb <= 0:
|
||||
continue
|
||||
try:
|
||||
nbr = _run(str(nb), None)
|
||||
chunk = _serialize(nbr.results, start_idx=base_idx)
|
||||
nearby_block_decisions.extend(chunk)
|
||||
base_idx += len(chunk)
|
||||
except Exception:
|
||||
logger.exception(f"nearby block {nb} lookup failed")
|
||||
|
||||
return {
|
||||
"total_in_db": response.total_results,
|
||||
"returned": len(decisions),
|
||||
"total_in_db": exact_response.total_results,
|
||||
"returned": len(exact_decisions),
|
||||
"filters": req.model_dump(exclude_none=True),
|
||||
"decisions": decisions,
|
||||
"decisions": exact_decisions,
|
||||
"same_block": {
|
||||
"count": len(same_block_decisions),
|
||||
"decisions": same_block_decisions,
|
||||
},
|
||||
"nearby_blocks": {
|
||||
"count": len(nearby_block_decisions),
|
||||
"decisions": nearby_block_decisions,
|
||||
},
|
||||
}
|
||||
|
||||
# ── /api/appraisals/pdf ───────────────────────────────────────────
|
||||
|
||||
Reference in New Issue
Block a user