fix(c/sedona-s2geography): Make geography ring orientation less surprising for rings with crossing edges - #1143
Conversation
…rings with crossing edges One polygon in the Overture divisions dataset came out inverted when read as a geography: ST_Area returned a negative value and ST_Within / ST_Intersects matched points anywhere on Earth, silently corrupting every spatial join against the table. The ring is digitized with an out-and-back tail whose return path is offset by ~10 m; interpreted as geodesics, the return path crosses the outgoing path once. The crossing collapses the ring's turning number to zero, so the curvature sign that NormalizeOrientation() used to decide each ring's winding direction was meaningless, and the shell was reversed in place, inverting its interior. Update the s2geography submodule to derive ring orientation from the sign of the ring's signed area instead. The signed area agrees with the curvature sign for every valid ring and additionally reflects the net winding direction for slightly invalid ones, and add a regression test against a self-crossing sliver ring. Fixes apache#1085
|
Can you check I would also tweak the phrasing of the title here...this is not fixing anything (invalid input causes undefined behaviour...also true for geometry with crossing edges), but does make geography functions less surprising for the current default behaviour of |
… and polygons with holes
…ath revision of the orientation fix
|
Good call on both counts. I added
So it did not make it faster — the signed area is ~3× the cost of the curvature per ring, and using it unconditionally is a real regression. I've updated paleolimbot/s2geography#125 with a hybrid instead: trust the curvature when Also retitled the PR per your phrasing note — agreed this is about making the default behaviour less surprising for input whose behaviour is undefined, not fixing defined behaviour. |
paleolimbot
left a comment
There was a problem hiding this comment.
Thank you!
Just double checking that the submodule points to paleolimbot/s2geography and not a fork (that doesn't show up in the diff for some reason).
|
Yes — .gitmodules still points at https://github.com/paleolimbot/s2geography.git |
Description
Fixes #1085.
Rings with crossing edges are invalid, and geography behaviour for them is undefined — but the default
ST_GeogFromWKB/ST_GeogFromWKTaccepts them, and the orientation heuristic could invert such a polygon's interior to cover nearly the entire sphere:ST_Areareturned a negative value andST_Within/ST_Intersectsmatched points anywhere on Earth, silently corrupting every spatial join against the table. This change makes the orientation heuristic derive the winding direction in a way that stays correct for this kind of input, at no cost to valid input.Root cause
Every geography UDF materializes WKB through
GeoArrowGeography::Init, which callsGeoArrowLaxPolygonShape::NormalizeOrientation()to make shells counterclockwise and holes clockwise. That function derived each ring's winding direction from the sign of its curvature (turning angle). The affected ring is digitized with an out-and-back tail whose return path is offset by ~10 m; interpreted as geodesics, the return path crosses the outgoing path once. The crossing collapses the ring's turning number to zero, making the curvature sign meaningless, so the shell was reversed in place and its interior inverted. (This also explains whyST_Reverse/ST_Normalizehad no effect: orientation is re-derived, and re-broken, on every evaluation.)Change
paleolimbot/s2geography#125 updates
NormalizeOrientation()to trust the curvature only when it is unambiguous and consult the ring's signed area otherwise: a valid ring has|curvature| == |2π − area(left side)|, so its curvature can only fall within(−π, π)when the ring encloses between a quarter and three quarters of the sphere. Outside that band the curvature sign is trusted, so the common case costs exactly what it costs today; inside it — unusually large valid rings, or invalid rings whose crossing edges collapse the turning number toward zero — the signed area decides, which reflects the ring's net winding direction even for self-crossing input. For every valid ring the decisions are identical to before.This PR bumps the s2geography submodule to that change, adds a Python regression test with a minimal self-crossing sliver ring, and adds
st_areabenchmarks forPolygon(500)andPolygonWithHole(500)(the existing benchmark only coveredPolygon(10)).paleolimbot/s2geography#125 has merged, and the submodule now points at the merged upstream commit (
3ab3dd9).Benchmarks
Criterion,
ST_Areaover batches of random valid polygons (Apple Silicon), relative to the curvature-only baseline at the current submodule pin:st_areaPolygon(10)st_areaPolygon(500)st_areaPolygonWithHole(500)Verification
Against the original 258-point ring from #1085:
ST_Area(geog)ST_Perimeter(geog)ST_Within(POINT (-150 0), geog)ST_Within(POINT (33.5 16.5), geog)cargo test -p sedona-s2geography: 68/68pytest tests/geography/: 1456 passedAlso filed #1154 for a geography
ST_IsValid/ST_MakeValidas the principled follow-up for repairing this class of input.