feat: use precise and fast area intersection algorithm - #202
Merged
esgn merged 6 commits intoOct 1, 2026
Merged
Conversation
LionelZoubritzky-IGN
force-pushed
the
136/more_spatial_extras/intersection_area
branch
from
September 29, 2026 15:02
2894470 to
4048fd8
Compare
LionelZoubritzky-IGN
changed the base branch from
136/more-spatial_extras
to
esgn/187-validations
September 29, 2026 15:02
LionelZoubritzky-IGN
force-pushed
the
136/more_spatial_extras/intersection_area
branch
from
September 29, 2026 15:32
4048fd8 to
0f2f0fb
Compare
LionelZoubritzky-IGN
force-pushed
the
esgn/187-validations
branch
from
September 29, 2026 16:00
92d4127 to
fe9006a
Compare
LionelZoubritzky-IGN
force-pushed
the
136/more_spatial_extras/intersection_area
branch
from
September 29, 2026 16:07
0f2f0fb to
0cd3d89
Compare
esgn
force-pushed
the
esgn/187-validations
branch
from
September 30, 2026 08:23
fe9006a to
66bb1f0
Compare
LionelZoubritzky-IGN
changed the base branch from
esgn/187-validations
to
136/more-spatial_extras
September 30, 2026 09:02
LionelZoubritzky-IGN
force-pushed
the
136/more_spatial_extras/intersection_area
branch
from
September 30, 2026 09:03
0cd3d89 to
293b00c
Compare
…ox split Replace the per-feature triangle clipping (FilterClipper and box halving) with src/helpers/intersectionArea.ts: - the filter rings are copied once into flat arrays; the tile cache clips them with an exact axis-aligned Sutherland-Hodgman instead of bboxClip - the feature is clipped to the bbox of the filter near it, and the common box is split in halves until one side has few positions; a box that one side covers entirely, or not at all, ends the recursion - at the leaves, the smaller side is triangulated and the other side's rings are clipped by each triangle; areas come from winding numbers, exact on the sphere and computed relative to a local origin - self-intersecting rings and oversized leaves fall back on @turf/intersect, which restores the bow tie test removed by 3615e1c Median time per request on real Geoplateforme data, 3615e1c -> this commit: 2.1 s -> 0.47 s (communes x departement 35), 1.9 s -> 0.36 s (departements x region 24), 636 -> 223 ms (5000 vegetation zones x departement 35). Results match GEOS 3.13 within 1e-8 of the feature area. - move dropEmptyRings to helpers/geojson.ts - drop clipRingToTriangle and triangulate from helpers/area.ts, now unused
npm run bench runs test/wfs/intersectionArea.bench.ts (vitest bench) through transformFeatureCollectionResponse, as for one request: small polygons inside and across a detailed boundary, large polygons with holes across it, and sectors sharing it position for position.
esgn
force-pushed
the
136/more_spatial_extras/intersection_area
branch
from
October 1, 2026 08:24
71602ad to
0655b8b
Compare
LionelZoubritzky-IGN
deleted the
136/more_spatial_extras/intersection_area
branch
October 1, 2026 09:11
esgn
added a commit
that referenced
this pull request
Oct 1, 2026
…intersection_area) (#187) * chore: remove internal types and simplify code of byId * feat: introduce queryIsGetFeaturesInput * feat: better guidance towards gpf_search_types when the LLM uses wrong typenames or field values * chore: avoid spatial filter names replication in code * chore: use Geometry instead of GeoJsonGeometryLike * refactor: defer EWKT serialization to CQL building * chore: thread full input to deriveFromGeometry * chore: thread resolvedGeometryRef to deriveFromGeometry * feat: add new options to spatial_extras * fix: exclude spatial_extra requiring spatial_filter from ById requests * chore: misc fixes * fix: clean invalid GeoJSON resulting from bboxClip * fix: minor error message fix Co-authored-by: Emmanuel S. <5435148+esgn@users.noreply.github.com> * perf: precompute spatial context * fix: ensure spatial_extras is mandatory in input schemas * fix: distinguish non-areal spatial filters in intersectionAreaWithSpatialFilter * fix: set verbatimModuleSyntax to import type instead of just import Co-authored-by: Emmanuel S. <5435148+esgn@users.noreply.github.com> * refactor: limit drift risk by avoiding a manual object copy Co-authored-by: Emmanuel S. <5435148+esgn@users.noreply.github.com> * feat: add type guard for spatial filters in compileQueryParts Co-authored-by: Emmanuel S. <5435148+esgn@users.noreply.github.com> * test: re-add isGeometryLike test for completely unknown type Co-authored-by: Emmanuel S. <5435148+esgn@users.noreply.github.com> * docs: update docstring of compileIntersectsFeatureSpatialFilter Co-authored-by: Emmanuel S. <5435148+esgn@users.noreply.github.com> * refactor: make FeatureCollectionPostProcessInput better-defined * test: make sure a spatial_extra that cannot be computed is returned as null * fix: make spatialFilterToGeometry properly error on missing resolved geometry Co-authored-by: Emmanuel S. <5435148+esgn@users.noreply.github.com> * docs: improve error case description of spatial_extra * refactor: update SpatialContext type to enforce non-nullable properties Co-authored-by: Emmanuel S. <5435148+esgn@users.noreply.github.com> * fix: simplify geometryToPolygons * fix(spatial_extras): null when not computable, 0 only for an empty overlap - intersection_area: null when the feature or the filter is not areal, or when the filter geometry could not be prepared; 0 only when both are areal and do not overlap - every extra is null on a missing or empty geometry - length sums the linear parts of a GeometryCollection; area of an empty Polygon is null - measures sum the parts of a GeometryCollection, overlaps included (JTS) - state the contract in deriveFromGeometry and in the LLM-facing descriptions - regenerate docs/mcp-tools.md * fix(spatial_extras): validate filter-dependent extras in the input schema - move "requires a spatial filter" from compileQueryParts to the zod refinement: invalid-tool-params instead of execution-error, raised before any catalog or network call - reject distance_to_filter with intersects_point_filter (always 0) - list only the compatible filters in the error message - state the rule in the spatial_extras description, regenerate docs/mcp-tools.md - move the related tests from compileQueryParts to the tool * fix(properties): skip spatial_extras validation when none is requested buildPropertyNameWithGeometry always passes an empty spatial_extras list, and the `!spatial_extras` guard let it through: the by-id layer tool and the proxy by-id resolve then read the geometry format and threw on an unlisted or missing one. Return early on an empty list, so already minted by-id layer URLs do not depend on the catalog format values. * fix(gpf_get_features): tell the LLM that spatial_extras only cover returned features - tool and spatial_extras descriptions: extras are computed after the query, on the returned features only, and cannot be used in where/order_by; a ranking or a sum needs numberReturned == numberMatched - "property does not exist" error: explain when the name is a spatial extra (message only, a real catalog column with that name is still accepted) * fix(spatial_extras): rename distance_to_filter to distance_to_filter_center for clarity * fix(spatial_extras): align descriptions and errors with the #136 brief - centroid: may fall outside a concave geometry, so an intersects_point_filter on it may return neither the feature nor what contains it - distance_to_filter_center: distance from the filter center to the closest point of the feature, 0 when the feature contains the center; list the center of each filter - intersection_area: null when the feature or the intersects_feature_filter reference has no areal part, 0 when they do not overlap; name the areal filters (box, disc, isochrone, reference) - rankings and sums: raise limit or restrict the spatial filter; for the N closest, use dwithin_point_filter with distance_to_filter_center, sort, and widen distance_m when fewer than N come back - catalog check: report every incompatible extra in a single error, in the order of the request, instead of one per call - bbox on a Point layer: fix the double period and the missing comma in the error message - regenerate docs/mcp-tools.md * fix(properties): do not send the LLM to gpf_get_features in the spatial extra hint The hint is shared by every tool that validates property names. From gpf_get_feature_by_id, which has its own spatial_extras, it pointed to another tool instead of the same call's spatial_extras; from gpf_count_features and the layer tools, it suggested a tool switch that cannot help. Say only that the name is computed via spatial_extras and is not usable in select, where or order_by. * docs(spatial_extras): give the bbox order and its mapping to bbox_filter - bbox: [ouest, sud, est, nord] in WGS84 lon/lat, matching the west, south, east and north fields of bbox_filter - regenerate docs/mcp-tools.md * fix(properties): word the bbox-on-Point error for several returned features gpf_get_features returns several features, so "La géométrie de l'objet" read as if a single one came back. Say "La géométrie renvoyée", as the other catalog errors of the same check already do. * fix(spatial_extras): reject extras that non-areal or single-point references rule out - intersection_area needs an areal intersects_feature reference: a linear, MultiPoint or Point reference is now an error instead of null for every feature - distance_to_filter_center is always 0 from a reference reduced to a single position (a Point, or a MultiPoint whose points are all the same): now an error - check the reference against its catalog format before fetching it, then against its fetched geometry, which alone settles a geometry-any type or a single-position MultiPoint - run the queried layer check before any network call (reference or isochrone fetch), so its errors now come before select, where and order_by errors - intersection_area description: the reference must be areal; regenerate docs/mcp-tools.md * feat: use precise and fast area intersection algorithm (#202) * feat: use precise and fast area intersection algorithm * perf: add FilterClipper for intersection_area computation * perf: subdivide boxes for intersection_area * perf(intersection_area): integrate both geometries over a recursive box split Replace the per-feature triangle clipping (FilterClipper and box halving) with src/helpers/intersectionArea.ts: - the filter rings are copied once into flat arrays; the tile cache clips them with an exact axis-aligned Sutherland-Hodgman instead of bboxClip - the feature is clipped to the bbox of the filter near it, and the common box is split in halves until one side has few positions; a box that one side covers entirely, or not at all, ends the recursion - at the leaves, the smaller side is triangulated and the other side's rings are clipped by each triangle; areas come from winding numbers, exact on the sphere and computed relative to a local origin - self-intersecting rings and oversized leaves fall back on @turf/intersect, which restores the bow tie test removed by 3615e1c Median time per request on real Geoplateforme data, 3615e1c -> this commit: 2.1 s -> 0.47 s (communes x departement 35), 1.9 s -> 0.36 s (departements x region 24), 636 -> 223 ms (5000 vegetation zones x departement 35). Results match GEOS 3.13 within 1e-8 of the feature area. - move dropEmptyRings to helpers/geojson.ts - drop clipRingToTriangle and triangulate from helpers/area.ts, now unused * test(intersection_area): add a benchmark on synthetic geometries npm run bench runs test/wfs/intersectionArea.bench.ts (vitest bench) through transformFeatureCollectionResponse, as for one request: small polygons inside and across a detailed boundary, large polygons with holes across it, and sectors sharing it position for position. * perf: return null instead of forwarding to turf on invalid geometries --------- Co-authored-by: esgn <5435148+esgn@users.noreply.github.com> * test: fix obsolete test requiring too high precision * fix(spatial_extras): compute the bbox intersection_area with the shared engine bbox_filter clipped each feature with bboxClip while the engine prepared in prepareSpatialContext for the same box went unused. Values match on valid geometries, rounding aside; a self-intersecting feature now gives null, as with the other filters. Drop @turf/bbox-clip. * fix(spatial_extras): do not cite get_features filters in the by-id description The centroid and bbox lines, shared by both spatial_extras descriptions, pointed to intersects_point_filter and bbox_filter, which gpf_get_feature_by_id rejects. They now only do so in gpf_get_features, whose text is unchanged. * docs(dev): list npm run bench in the commands table --------- Co-authored-by: Emmanuel S. <5435148+esgn@users.noreply.github.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Description
Change the area intersection algorithm used for the
intersection_areaspatial_extrato make it faster and more precise.Related issues (if applicable)
Related to #187, #136
Motivation
Review of #136: if
intersection_areais required on a GpfGetFeatures query with many large geometries, the computation takes very long and stalls the entire MCP.Implementation
For each returned geometry : take the filter, clip it to the bbox of the geometry. Then, triangulate the geometry (with
earcut) and for each triangle, add the area of the intersection of the triangle with the clipped filter. This approach is much faster than relying on@turf/areaover@turf/intersect, because:The
areaalgorithm itself is also reimplemented to make sure it works on degenerate cases. This is necessary because the Sutherland–Hodgman algorithm can produce degenerate edges along a triangle side.Testing
Typecheck OK, 471 + 35 unit tests all green, docs regenerated (unchanged).
TODOs
This does not fix the fact that the MCP stalls while computing the intersection, but in practice it should make performance fast enough for that not to matter... To be checked afterwards.
Checklist