From b56cf12ca2f7341004e0efbbd194aebcc00537d2 Mon Sep 17 00:00:00 2001 From: Brian Bergstrom Date: Sat, 22 Aug 2026 14:57:32 -0500 Subject: [PATCH] fix: recover polygon area from GeometryCollection intersections in grid trim Fixes #6638. GridService.trim_grid_to_aoi() drops a partially-overlapping tile entirely whenever its intersection with the AOI comes back as anything other than a bare Polygon/MultiPolygon. But a tile touching the AOI boundary can intersect into a GeometryCollection containing a real polygon slice plus stray Point/LineString artifacts from the boundary touch - the old code discarded the whole tile in that case, losing real area and leaving gaps where the task grid should have covered the AOI. Diagnosed by @prabinoid in the issue thread; this implements the fix they identified: for a GeometryCollection result, filter .geoms for just the Polygon/MultiPolygon members and unary_union them back into a single shape to keep using. If none of the sub-geometries carry any area (a pure boundary touch), the tile is still skipped, same as before. Verified against a constructed reproduction (two AOI polygons - one overlapping the tile's interior, one only touching its edge - unioned together) that actually produces a GeometryCollection([Polygon, LineString]) intersection with Shapely, matching the reported bug exactly: old logic drops the tile despite 1.0 unit of real overlap area; new logic recovers it. Also verified the "no polygon in the collection" and "genuinely no overlap" paths still correctly skip the tile, unchanged from before. Wasn't able to run the existing test_grid_service.py suite locally - importing the backend package pulls in its full dependency tree (mail clients etc.) beyond what this module itself needs, and BaseTestCase additionally requires a live Postgres instance neither of which were available in my environment. The verification above exercises the exact Shapely operations this change makes; deferring the full suite to CI. Co-Authored-By: Claude Sonnet 5 Signed-off-by: Brian Bergstrom --- backend/services/grid/grid_service.py | 22 +++++++++++++++++----- 1 file changed, 17 insertions(+), 5 deletions(-) diff --git a/backend/services/grid/grid_service.py b/backend/services/grid/grid_service.py index 7a634a42a0..0a98f6e830 100644 --- a/backend/services/grid/grid_service.py +++ b/backend/services/grid/grid_service.py @@ -46,11 +46,23 @@ def trim_grid_to_aoi(grid_dto: GridDTO) -> geojson.FeatureCollection: intersecting_features.append(feature) else: intersection = aoi_multi_polygon.intersection(tile) - if intersection.is_empty or intersection.geom_type not in [ - "Polygon", - "MultiPolygon", - ]: - continue # this intersections which are not polygons or which are completely outside aoi + if intersection.is_empty: + continue # tile is completely outside aoi + if intersection.geom_type == "GeometryCollection": + # A tile touching the AOI boundary can intersect into a mix of + # geometry types (e.g. a real polygon slice plus stray points/ + # lines from the boundary touch). Recover the polygon area + # instead of discarding the whole tile. + polygonal_parts = [ + geom + for geom in intersection.geoms + if geom.geom_type in ("Polygon", "MultiPolygon") + ] + if not polygonal_parts: + continue # no actual area in the intersection, nothing to keep + intersection = unary_union(polygonal_parts) + elif intersection.geom_type not in ["Polygon", "MultiPolygon"]: + continue # intersection is a bare LineString/Point, no area # tile is partially intersecting the aoi clipped_feature = GridService._update_feature( clip_to_aoi, feature, intersection