Repository navigation
ST_Touches returns wrong results for mixed-dimension GeometryCollections #1383
Description
ST_Touches on geometry returns the wrong answer when one side is a GeometryCollection whose parts have different dimensions. The result is wrong in both directions: it reports touches where an interior intersects, and misses touches made only by the lower-dimension part. The wrong values are returned silently, with no error. Swapping the arguments doesn't help: ST_Touches(a, gc) and ST_Touches(gc, a) give the same wrong answer.
Found while testing the sedonadb-geopandas touches wrapper in #1355.
Repro
import sedona.db
sd = sedona.db.connect()
sd.sql("""
SELECT ST_Touches(
ST_GeomFromWKT('POLYGON ((0 0, 2 0, 2 2, 0 2, 0 0))'),
ST_GeomFromWKT('GEOMETRYCOLLECTION (POINT (1 1), LINESTRING (0 0, 1 0))')
) AS touches
""").show()
# touches = trueExpected false. The line lies on the polygon's boundary, but POINT (1 1) is inside the polygon, so the interiors intersect:
import shapely
shapely.touches(
shapely.from_wkt("POLYGON ((0 0, 2 0, 2 2, 0 2, 0 0))"),
shapely.from_wkt("GEOMETRYCOLLECTION (POINT (1 1), LINESTRING (0 0, 1 0))"),
) # FalseAll mismatches
Out of 225 ordered pairs from a mixed corpus (points, lines, polygons including one with a hole, multi-geometries, empties, Z points, and one GeometryCollection), all 6 mismatches involve GC = GEOMETRYCOLLECTION (POINT (1 1), LINESTRING (0 0, 1 0)):
| a | b | SedonaDB ST_Touches(a, b) |
shapely.touches(a, b) |
|---|---|---|---|
LINESTRING (0 0, 2 2) |
GC | true | false |
POLYGON ((0 0, 2 0, 2 2, 0 2, 0 0)) |
GC | true | false |
POLYGON ((1 1, 3 1, 3 3, 1 3, 1 1)) |
GC | false | true |
| GC | LINESTRING (0 0, 2 2) |
true | false |
| GC | POLYGON ((0 0, 2 0, 2 2, 0 2, 0 0)) |
true | false |
| GC | POLYGON ((1 1, 3 1, 3 3, 1 3, 1 1)) |
false | true |
In each case the correct answer depends on POINT (1 1):
- It lies in the interior of the line in row 1 and in the interior of the polygon in row 2, so neither touches.
- In row 3, it is the polygon's corner and is the only shared point, so they touch.
Collections whose parts all have the same dimension agree with Shapely. For example, GEOMETRYCOLLECTION (LINESTRING (0 0, 1 0)) and GEOMETRYCOLLECTION (POINT (1 1)) against these polygons both give the correct answer.
Where it happens
The tg kernel handles this case. c/sedona-tg/src/register.rs registers st_touches. rust/sedona/src/context.rs registers the tg kernels after the GEOS ones, and SedonaScalarUDF tries kernels newest first (rust/sedona-expr/src/scalar_udf.rs), so tg wins over c/sedona-geos for geometry.
In c/sedona-tg/src/tg/tg.c, tg_geom_touches swaps the arguments so the collection is always first. That explains why both argument orders fail the same way. geometrycollection_touches_geom then only visits the collection's parts whose dimension is at least the collection's own dimension (collection_dim). In geometrycollection_touches_iter, that is the tg_geom_de9im_dims(geom) >= ctx->collection_dim check. So the 0-D POINT (1 1) in a 1-D collection is never tested. This would explain every row in the table:
- Rows 1–2: the point's interior intersection is missed, so the result is
true. - Row 3: the point's touch is missed and the line is disjoint, so the result is
false.
This rule was added on purpose in tidwall/tg#32 ("only the highest dimension counts towards the 'interior'"). It gives the right answer when the lower-dimension parts lie on a higher-dimension part, which is what every new case in #32's gcol.jsonc does. For example, POINT (0 0) lies on LINESTRING (0 0, 0 1). It gives the wrong answer when a lower-dimension part is not covered, as here. Under OGC/GEOS, an uncovered point still belongs to the collection's interior. A fix probably needs to check lower-dimension parts against the union of the higher-dimension parts, and drop only the ones that are covered. Neither open tg PR (tidwall/tg#33, tidwall/tg#35) touches this path.
Related: #1035 (closed) was a similar mixed-dimension GeometryCollection bug in tg's contains.
Versions
- SedonaDB built from main at
db8a30146(includes the tg update in chore(c/sedona-tg): Update tg #1353) - Expected values from Shapely 2.1.2 / GEOS 3.13.1
Activity
fix(python/sedonadb-geopandas): drop workarounds the engine fixed, tr…
jiayuasu commentedon Oct 2, 2026
This looks like a regression from the tg update (#1353). Released sedonadb 0.4.1, which predates it, gets four of the five cases in the table above right; a build of current main gets all five wrong:
LINESTRING (0 0, 2 2)POLYGON ((0 0, 2 0, 2 2, 0 2, 0 0))POLYGON ((1 1, 3 1, 3 3, 1 3, 1 1))LINESTRING (0 0, 2 2)POLYGON ((0 0, 2 0, 2 2, 0 2, 0 0))sedonadb-geopandas(#1355) records the polygon case as a strict expected failure on engines with the update, so its test will flag when this is fixed.fix(python/sedonadb-geopandas): drop workarounds the engine fixed, tr…