-
Notifications
You must be signed in to change notification settings - Fork 0
fix: ten review findings, each one a silent wrong answer #136
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
2f3f869
fb86d2f
a548054
7f741ca
cc2c9f3
65c0a54
f0a1f69
a5f9b3f
cbf9e64
5738ed2
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -118,7 +118,7 @@ def wfs_status(base: str, *, session: Any = None, timeout: int = 60) -> dict[str | |
| module: it returns real geometry for every feature type, including the lines | ||
| and polygons that WMS extraction cannot honestly recover. | ||
| """ | ||
| sess = session or make_session() | ||
| sess = session if session is not None else make_session() | ||
| out: dict[str, Any] = {"enabled": False, "versions": {}, "message": None} | ||
| for version in ("2.0.0", "1.1.0", "1.0.0"): | ||
| url = f"{base.rstrip('/')}/wfs?" + urlencode( | ||
|
|
@@ -178,6 +178,47 @@ def offset(self, fraction: float = 0.5) -> "Tile": | |
| self.east + dx, self.north + dy, self.depth) | ||
|
|
||
|
|
||
| def _point_in(feature: dict, bbox: Sequence[float]) -> bool | None: | ||
| """Whether a feature's point lies in `bbox`. None when it cannot be judged. | ||
|
|
||
| The verification grid starts half a cell BEFORE the region, so it queries | ||
| ground outside it. A feature there was never this extraction's to find, and | ||
| counting it as a first-pass miss makes a complete sweep report | ||
| `saturated=False`. | ||
|
|
||
| A feature with no point geometry returns None. It is kept and counted, | ||
| because dropping it would hide a real miss and keeping it silently would | ||
| hide the doubt. | ||
| """ | ||
| geom = (feature or {}).get("geometry") or {} | ||
| coords = geom.get("coordinates") | ||
| kind = geom.get("type") | ||
| # A point layer serves MultiPoint as readily as Point, and treating that as | ||
| # unplaceable let an out-of-region feature count as a first-pass miss. Any | ||
| # other geometry stays unplaceable: a polygon has no single answer here, and | ||
| # guessing one would be the invented geometry this module refuses. | ||
| if kind == "Point": | ||
| points = [coords] | ||
| elif kind == "MultiPoint" and isinstance(coords, (list, tuple)): | ||
| points = list(coords) | ||
| else: | ||
| return None | ||
|
|
||
| placed = False | ||
| for point in points: | ||
| if not isinstance(point, (list, tuple)) or len(point) < 2: | ||
| continue | ||
| try: | ||
| lon, lat = float(point[0]), float(point[1]) | ||
| except (TypeError, ValueError): | ||
| continue | ||
| placed = True | ||
| # One point inside makes the feature this region's. | ||
| if bbox[0] <= lon <= bbox[2] and bbox[1] <= lat <= bbox[3]: | ||
| return True | ||
| return False if placed else None | ||
|
|
||
|
|
||
| @dataclass | ||
| class GeoServer: | ||
| """A WMS-only GeoServer, swept for point features. | ||
|
|
@@ -195,7 +236,7 @@ class GeoServer: | |
| _stats: dict[str, int] = field(default_factory=lambda: {"requests": 0, "capped": 0}) | ||
|
|
||
| def __post_init__(self) -> None: | ||
| self.session = self.session or make_session() | ||
| self.session = self.session if self.session is not None else make_session() | ||
| self.base = self.base.rstrip("/") | ||
|
|
||
| # ---------------------------------------------------------------- discovery | ||
|
|
@@ -248,7 +289,8 @@ def features_at(self, layer: str, tile: Tile, *, timeout: int = 120) -> list[dic | |
| def sweep(self, layer: str, bbox: Sequence[float], *, start_span: float = 2.0, | ||
| min_span: float = 1 / 32, key: str | None = None, | ||
| on_batch: Callable[[dict[str, dict]], None] | None = None, | ||
| tolerate_tile_errors: bool = True) -> dict[str, dict]: | ||
| tolerate_tile_errors: bool = True, | ||
| status: dict | None = None) -> dict[str, dict]: | ||
| """Recursively subdivide ``bbox`` until no tile is capped, collecting points. | ||
|
|
||
| ``key`` names the attribute that identifies a feature (a school code, an | ||
|
|
@@ -276,6 +318,7 @@ def sweep(self, layer: str, bbox: Sequence[float], *, start_span: float = 2.0, | |
|
|
||
| found: dict[str, dict] = {} | ||
| failures: list[tuple[Tile, str]] = [] | ||
| capped: list[Tile] = [] | ||
| while queue: | ||
| tile = queue.pop() | ||
| try: | ||
|
|
@@ -288,19 +331,39 @@ def sweep(self, layer: str, bbox: Sequence[float], *, start_span: float = 2.0, | |
| if len(feats) >= self.feature_count and tile.span > min_span: | ||
| queue.extend(tile.quarter()) | ||
| continue | ||
| if len(feats) >= self.feature_count: | ||
| # The tile is at `min_span` and still capped, so the walk cannot | ||
| # subdivide further. A cap means "there may be more", so this | ||
| # leaf is INCOMPLETE. Ingesting it as a complete result truncates | ||
| # the densest clusters, which are the ones a reader most wants. | ||
| capped.append(tile) | ||
| for f in feats: | ||
| props = f.get("properties", {}) or {} | ||
| ident = str(props.get(key)) if key else str(f.get("id")) | ||
| if ident and ident not in ("None", ""): | ||
| found[ident] = props | ||
| # Keep the WHOLE feature. Storing only `properties` dropped | ||
| # the coordinates, and a point extractor that returns no | ||
| # points answers a different question than the one asked. | ||
| found[ident] = f | ||
| if on_batch and len(found) % 500 < len(feats): | ||
| on_batch(found) | ||
|
|
||
| if failures: | ||
| self.log(f" {layer}: {len(failures)} tile(s) failed and were SKIPPED — " | ||
| f"this sweep is PARTIAL, not complete") | ||
| for t, msg in failures[:5]: | ||
| self.log(f" {t.west},{t.south},{t.east},{t.north}: {msg}") | ||
| for tl, msg in failures[:5]: | ||
| self.log(f" {tl.west},{tl.south},{tl.east},{tl.north}: {msg}") | ||
| if capped: | ||
| self.log(f" {layer}: {len(capped)} tile(s) hit the feature cap at the " | ||
| f"minimum span — this sweep is PARTIAL, and those tiles are " | ||
| f"a LOWER BOUND") | ||
| if status is not None: | ||
| # The caller cannot see a log line. `verify` must not certify a | ||
| # sweep it cannot see the holes in. | ||
| box = lambda t: (t.west, t.south, t.east, t.north) # noqa: E731 | ||
| status.update(failed=[box(t) for t, _ in failures], | ||
| capped=[box(t) for t in capped], | ||
| partial=bool(failures or capped)) | ||
| return found | ||
|
|
||
| def verify(self, layer: str, bbox: Sequence[float], known: Iterable[str], *, | ||
|
|
@@ -314,16 +377,56 @@ def verify(self, layer: str, bbox: Sequence[float], known: Iterable[str], *, | |
| """ | ||
| known = set(known) | ||
| west, south, east, north = bbox | ||
| shifted = Tile(west, south, east, north).offset(0.5) | ||
| got = self.sweep(layer, (shifted.west, shifted.south, shifted.east, shifted.north), | ||
| start_span=start_span, key=key) | ||
| # Shift by half a CELL, not half the region: a 4-degree box with | ||
| # 2-degree cells must move 1 degree. Moving half the region queries | ||
| # ground outside it and leaves the leading edge untested. | ||
| # | ||
| # And never by more than half the region itself. A box smaller than one | ||
| # cell would otherwise move clean off its own ground: (76,12,77,13) with | ||
| # a 2-degree cell became (77,13,78,14), where finding nothing new | ||
| # certifies saturation over a region this layer never claimed. Bounded | ||
| # per axis, because a box can be wide and short. | ||
| step_x = min(start_span, east - west) / 2 | ||
| step_y = min(start_span, north - south) / 2 | ||
| # Shift the grid ORIGIN backwards, not the whole box forwards. Moving | ||
| # the box east and north offsets the cell boundaries, but it also | ||
| # leaves the western and southern strips of the region with no second | ||
| # pass at all — and saturation is claimed for the WHOLE region. Starting | ||
| # half a cell before the region keeps the offset and covers every part | ||
| # of it. | ||
| shifted = (west - step_x, south - step_y, east, north) | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
When features exist immediately west or south of the requested bbox, the fresh backward shift queries them as part of the verification pass, and Useful? React with 👍 / 👎. |
||
| status: dict = {} | ||
| swept = self.sweep(layer, shifted, start_span=start_span, key=key, status=status) | ||
| got, unlocatable = {}, 0 | ||
| for ident, feature in swept.items(): | ||
| verdict = _point_in(feature, bbox) | ||
| if verdict is False: | ||
| continue | ||
| if verdict is None: | ||
| unlocatable += 1 | ||
| got[ident] = feature | ||
| new = set(got) - known | ||
| partial = bool(status.get("partial")) | ||
| if partial: | ||
| self.log(f" {layer}: the verification pass is itself PARTIAL — " | ||
| f"{len(status.get('failed', []))} failed and " | ||
| f"{len(status.get('capped', []))} capped tile(s). " | ||
| "Saturation is NOT claimed.") | ||
| return { | ||
| "pass1": len(known), | ||
| "pass2": len(got), | ||
| "new": len(new), | ||
| "recall": (len(known & set(got)) / len(known)) if known else 0.0, | ||
| "saturated": not new, | ||
| # An empty `new` proves saturation only when the second pass | ||
| # actually asked every question. A pass with holes produces the | ||
| # same empty set for the opposite reason. | ||
| "saturated": not new and not partial, | ||
| "partial": partial, | ||
| # Features the second pass could not place. They are counted as | ||
| # in-region, so this number is the doubt in `new`. | ||
| "unlocatable": unlocatable, | ||
| "failed_tiles": status.get("failed", []), | ||
| "capped_tiles": status.get("capped", []), | ||
| "new_ids": sorted(new)[:50], | ||
| } | ||
|
|
||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
When a 403 response contains one of the geo-fence markers, this branch now raises
GeoFenced, butfetch()still tells callers that every 403 represents an absent object, cannot be distinguished, and returnsNone. Callers following that public contract may be surprised by the new exception, so document the split between ordinary 403 responses and geo-fenced responses.Useful? React with 👍 / 👎.