Skip to content

ST_Touches returns wrong results for mixed-dimension GeometryCollections #1383

Open

Description

@james-willis

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 = true

Expected 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))"),
)  # False

All 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

Activity

  1. jiayuasu commented on Oct 2, 2026

    @jiayuasu
    Member

    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:

    a b 0.4.1 main Shapely
    LINESTRING (0 0, 2 2) GC true true false
    POLYGON ((0 0, 2 0, 2 2, 0 2, 0 0)) GC false true false
    POLYGON ((1 1, 3 1, 3 3, 1 3, 1 1)) GC true false true
    GC LINESTRING (0 0, 2 2) false true false
    GC POLYGON ((0 0, 2 0, 2 2, 0 2, 0 0)) false true false

    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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions