Model/repositories/geospatial/geospatial_repository.py
Khalim Conn-Kowlessar 95719dd587 feat(geospatial): batch coordinates_for_uprns lookup (#1227)
Adds GeospatialRepository.coordinates_for_uprns(uprns) -> dict — a batch
coordinate lookup returning only covered UPRNs. The S3 adapter overrides it
to read the meta once, group UPRNs by their covering partition, and read each
partition once for all the UPRNs it covers; co-located (closely-numbered)
UPRNs share a partition, so an EPC Prediction cohort is typically one or two
reads instead of one per neighbour. Default port impl is a per-UPRN loop.

Feeds the EPC Prediction geo-proximity work: a cohort's UPRNs resolve to
coordinates in a couple of reads (validated at corpus scale: 170 partition
reads for 2683 UPRNs).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-15 14:35:32 +00:00

48 lines
2.1 KiB
Python

from __future__ import annotations
from abc import ABC, abstractmethod
from typing import Optional
from domain.geospatial.coordinates import Coordinates
from domain.geospatial.planning_restrictions import PlanningRestrictions
from domain.geospatial.spatial_reference import SpatialReference
class GeospatialRepository(ABC):
"""Resolves a Property's coordinates from hosted reference data by UPRN.
A Repo, not a Fetcher (ADR-0011): it reads stored Ordnance Survey Open-UPRN
data, with no live API call. Returns None when the UPRN is not covered.
"""
@abstractmethod
def coordinates_for(self, uprn: int) -> Optional[Coordinates]: ...
def coordinates_for_uprns(
self, uprns: list[int]
) -> dict[int, Coordinates]:
"""Resolve many UPRNs at once, returning only those covered. The default
is a per-UPRN loop; adapters whose storage is partitioned (e.g. the S3
Open-UPRN parquet) override this to read each partition once for all the
UPRNs it covers — far fewer reads when the UPRNs are co-located, as
closely-numbered UPRNs share a partition."""
resolved: dict[int, Coordinates] = {}
for uprn in uprns:
coordinates = self.coordinates_for(uprn)
if coordinates is not None:
resolved[uprn] = coordinates
return resolved
def spatial_for(self, uprn: int) -> Optional[SpatialReference]:
"""The Property's coordinates and planning protections together, in one
reference lookup (ADR-0020) — Ingestion uses the coordinates to drive
the Solar fetch and persists the whole reference. Defaults to None so
reference sources that don't carry the flags need not implement it."""
return None
def planning_restrictions_for(self, uprn: int) -> Optional[PlanningRestrictions]:
"""The Property's planning protections (conservation/listed/heritage),
co-located with the coordinates in the reference data (ADR-0020).
Defaults to None (unknown → unrestricted) so reference sources that
don't carry the flags need not implement it."""
return None