[SCM] PostGIS branch master updated. 3.6.0rc2-619-g6f291bf54
git at osgeo.org
git at osgeo.org
Thu Jun 18 15:17:39 PDT 2026
This is an automated email from the git hooks/post-receive script. It was
generated because a ref change was pushed to the repository containing
the project "PostGIS".
The branch, master has been updated
via 6f291bf547cf9b1834cfa63a779899f6ede4aeba (commit)
from 63cbf4f069afebe50cb520b7abe6440c898842e9 (commit)
Those revisions listed above that are new to this repository have
not appeared on any other notification email; so we list those
revisions in full, below.
- Log -----------------------------------------------------------------
commit 6f291bf547cf9b1834cfa63a779899f6ede4aeba
Author: Darafei Praliaskouski <me at komzpa.net>
Date: Thu Jun 18 16:46:37 2026 +0400
Improve point-only GeometryCollection predicate fast paths
References #4749
Closes https://github.com/postgis/postgis/pull/966
diff --git a/NEWS b/NEWS
index 2e4b7cac6..24b7e37b5 100644
--- a/NEWS
+++ b/NEWS
@@ -57,6 +57,8 @@ To take advantage of all postgis_sfcgal extension features SFCGAL 2.3+ is needed
(Sandro Santilli)
- #3743, [raster] Document and test raster2pgsql -s FROM_SRID:SRID
reprojection support (Darafei Praliaskouski)
+ - #4749, Use point-in-polygon predicate fast paths for point-only
+ GeometryCollections (Darafei Praliaskouski)
* Bug Fixes *
diff --git a/postgis/lwgeom_geos_predicates.c b/postgis/lwgeom_geos_predicates.c
index 87d34a694..2a283e17c 100644
--- a/postgis/lwgeom_geos_predicates.c
+++ b/postgis/lwgeom_geos_predicates.c
@@ -18,13 +18,13 @@
*
**********************************************************************
*
+ * Copyright 2026 Darafei Praliaskouski <me at komzpa.net>
* Copyright 2009-2014 Sandro Santilli <strk at kbt.io>
* Copyright 2008 Paul Ramsey <pramsey at cleverelephant.ca>
* Copyright 2001-2003 Refractions Research Inc.
*
**********************************************************************/
-
#include "../postgis_config.h"
/* PostgreSQL */
@@ -73,20 +73,98 @@ is_poly(const GSERIALIZED *g)
}
/*
- * Utility to quickly check for point geometries
+ * Cheap type gate for geometries that may become point-like.
*/
static inline uint8_t
-is_point(const GSERIALIZED *g)
+is_point_or_collection(const GSERIALIZED *g)
{
int type = gserialized_get_type(g);
- return type == POINTTYPE || type == MULTIPOINTTYPE;
+ return type == POINTTYPE || type == MULTIPOINTTYPE || type == COLLECTIONTYPE;
}
+/* Avoid homogenizing mixed collections just to discover that PIP cannot use them. */
+static uint8_t
+lwgeom_is_pointlike(const LWGEOM *lwgeom)
+{
+ const LWCOLLECTION *lwcol;
+ uint32_t i;
+ if (lwgeom->type == POINTTYPE || lwgeom->type == MULTIPOINTTYPE)
+ return LW_TRUE;
+ if (lwgeom->type != COLLECTIONTYPE)
+ return LW_FALSE;
+ lwcol = (const LWCOLLECTION *)lwgeom;
+ if (lwcol->ngeoms == 0)
+ return LW_FALSE;
+ for (i = 0; i < lwcol->ngeoms; i++)
+ {
+ if (!lwgeom_is_pointlike(lwcol->geoms[i]))
+ return LW_FALSE;
+ }
+ return LW_TRUE;
+}
+
+/*
+ * Return an owned point/multipoint LWGEOM for direct point inputs or for
+ * GeometryCollections whose children are all points.
+ */
+static LWGEOM *
+pointlike_lwgeom_from_gserialized(const GSERIALIZED *g)
+{
+ int type = gserialized_get_type(g);
+ LWGEOM *lwgeom;
+ LWGEOM *hgeom;
+
+ if (type == POINTTYPE || type == MULTIPOINTTYPE)
+ return lwgeom_from_gserialized(g);
+
+ if (type != COLLECTIONTYPE)
+ return NULL;
+
+ lwgeom = lwgeom_from_gserialized(g);
+ if (!lwgeom_is_pointlike(lwgeom))
+ {
+ lwgeom_free(lwgeom);
+ return NULL;
+ }
+
+ hgeom = lwgeom_homogenize(lwgeom);
+ lwgeom_free(lwgeom);
+
+ if (!hgeom)
+ return NULL;
+
+ if (hgeom->type == POINTTYPE || hgeom->type == MULTIPOINTTYPE)
+ return hgeom;
+
+ lwgeom_free(hgeom);
+ return NULL;
+}
+
+typedef bool (*itree_pip_predicate)(const IntervalTree *itree, const LWGEOM *lwpoints);
+
+static bool
+try_itree_pointlike_pip(FunctionCallInfo fcinfo,
+ const GSERIALIZED *gpoints,
+ SHARED_GSERIALIZED *shared_gpoly,
+ itree_pip_predicate predicate,
+ bool *result)
+{
+ LWGEOM *lwpt = pointlike_lwgeom_from_gserialized(gpoints);
+ IntervalTree *itree;
+
+ if (!lwpt)
+ return false;
+
+ itree = GetIntervalTree(fcinfo, shared_gpoly);
+ *result = predicate(itree, lwpt);
+ lwgeom_free(lwpt);
+ return true;
+}
PG_FUNCTION_INFO_V1(ST_Intersects);
Datum ST_Intersects(PG_FUNCTION_ARGS)
@@ -98,6 +176,7 @@ Datum ST_Intersects(PG_FUNCTION_ARGS)
int8_t result;
GBOX box1, box2;
PrepGeomCache *prep_cache;
+ bool pip_result;
gserialized_error_if_srid_mismatch(geom1, geom2, __func__);
@@ -117,20 +196,18 @@ Datum ST_Intersects(PG_FUNCTION_ARGS)
}
/*
- * Short-circuit 2: if the geoms are a point and a polygon,
- * call the itree_pip_intersects function.
+ * Short-circuit 2: if one geometry is polygonal and the other is a
+ * point or point-only collection, use the IntervalTree PIP fast path.
*/
- if ((is_point(geom1) && is_poly(geom2)) ||
- (is_point(geom2) && is_poly(geom1)))
+ if (is_point_or_collection(geom1) && is_poly(geom2) &&
+ try_itree_pointlike_pip(fcinfo, geom1, shared_geom2, itree_pip_intersects, &pip_result))
{
- SHARED_GSERIALIZED *shared_gpoly = is_poly(geom1) ? shared_geom1 : shared_geom2;
- SHARED_GSERIALIZED *shared_gpoint = is_point(geom1) ? shared_geom1 : shared_geom2;
- const GSERIALIZED *gpoint = shared_gserialized_get(shared_gpoint);
- LWGEOM *lwpt = lwgeom_from_gserialized(gpoint);
- IntervalTree *itree = GetIntervalTree(fcinfo, shared_gpoly);
- bool result = itree_pip_intersects(itree, lwpt);
- lwgeom_free(lwpt);
- PG_RETURN_BOOL(result);
+ PG_RETURN_BOOL(pip_result);
+ }
+ else if (is_point_or_collection(geom2) && is_poly(geom1) &&
+ try_itree_pointlike_pip(fcinfo, geom2, shared_geom1, itree_pip_intersects, &pip_result))
+ {
+ PG_RETURN_BOOL(pip_result);
}
initGEOS(lwpgnotice, lwgeom_geos_error);
@@ -504,6 +581,7 @@ Datum contains(PG_FUNCTION_ARGS)
GEOSGeometry *g1, *g2;
GBOX box1, box2;
PrepGeomCache *prep_cache;
+ bool pip_result;
gserialized_error_if_srid_mismatch(geom1, geom2, __func__);
@@ -525,17 +603,13 @@ Datum contains(PG_FUNCTION_ARGS)
}
/*
- ** Short-circuit 2: if geom2 is a point and geom1 is a polygon
- ** call the point-in-polygon function.
+ ** Short-circuit 2: if geom1 is polygonal and geom2 is a point
+ ** or point-only collection, use the IntervalTree PIP fast path.
*/
- if (is_poly(geom1) && is_point(geom2))
+ if (is_poly(geom1) && is_point_or_collection(geom2) &&
+ try_itree_pointlike_pip(fcinfo, geom2, shared_geom1, itree_pip_contains, &pip_result))
{
- const GSERIALIZED *gpoint = shared_gserialized_get(shared_geom2);
- LWGEOM *lwpt = lwgeom_from_gserialized(gpoint);
- IntervalTree *itree = GetIntervalTree(fcinfo, shared_geom1);
- bool result = itree_pip_contains(itree, lwpt);
- lwgeom_free(lwpt);
- PG_RETURN_BOOL(result);
+ PG_RETURN_BOOL(pip_result);
}
initGEOS(lwpgnotice, lwgeom_geos_error);
@@ -585,6 +659,7 @@ Datum within(PG_FUNCTION_ARGS)
GEOSGeometry *g1, *g2;
GBOX box1, box2;
PrepGeomCache *prep_cache;
+ bool pip_result;
gserialized_error_if_srid_mismatch(geom1, geom2, __func__);
@@ -606,17 +681,13 @@ Datum within(PG_FUNCTION_ARGS)
}
/*
- ** Short-circuit 2: if geom2 is a polygon and geom1 is a point
- ** call the point-in-polygon function.
+ ** Short-circuit 2: if geom2 is polygonal and geom1 is a point
+ ** or point-only collection, use the IntervalTree PIP fast path.
*/
- if (is_poly(geom2) && is_point(geom1))
+ if (is_poly(geom2) && is_point_or_collection(geom1) &&
+ try_itree_pointlike_pip(fcinfo, geom1, shared_geom2, itree_pip_contains, &pip_result))
{
- const GSERIALIZED *gpoint = shared_gserialized_get(shared_geom1);
- LWGEOM *lwpt = lwgeom_from_gserialized(gpoint);
- IntervalTree *itree = GetIntervalTree(fcinfo, shared_geom2);
- bool result = itree_pip_contains(itree, lwpt);
- lwgeom_free(lwpt);
- PG_RETURN_BOOL(result);
+ PG_RETURN_BOOL(pip_result);
}
initGEOS(lwpgnotice, lwgeom_geos_error);
@@ -729,6 +800,7 @@ Datum covers(PG_FUNCTION_ARGS)
int8_t result;
GBOX box1, box2;
PrepGeomCache *prep_cache;
+ bool pip_result;
POSTGIS_DEBUGF(3, "Covers: type1: %d, type2: %d", gserialized_get_type(geom1), gserialized_get_type(geom2));
@@ -751,17 +823,13 @@ Datum covers(PG_FUNCTION_ARGS)
}
}
/*
- * Short-circuit 2: if geom2 is a point and geom1 is a polygon
- * call the point-in-polygon function.
+ * Short-circuit 2: if geom1 is polygonal and geom2 is a point
+ * or point-only collection, use the IntervalTree PIP fast path.
*/
- if (is_poly(geom1) && is_point(geom2))
+ if (is_poly(geom1) && is_point_or_collection(geom2) &&
+ try_itree_pointlike_pip(fcinfo, geom2, shared_geom1, itree_pip_covers, &pip_result))
{
- const GSERIALIZED *gpoint = shared_gserialized_get(shared_geom2);
- LWGEOM *lwpt = lwgeom_from_gserialized(gpoint);
- IntervalTree *itree = GetIntervalTree(fcinfo, shared_geom1);
- bool result = itree_pip_covers(itree, lwpt);
- lwgeom_free(lwpt);
- PG_RETURN_BOOL(result);
+ PG_RETURN_BOOL(pip_result);
}
initGEOS(lwpgnotice, lwgeom_geos_error);
@@ -813,6 +881,7 @@ Datum coveredby(PG_FUNCTION_ARGS)
int8_t result;
GBOX box1, box2;
PrepGeomCache *prep_cache;
+ bool pip_result;
gserialized_error_if_srid_mismatch(geom1, geom2, __func__);
@@ -835,17 +904,13 @@ Datum coveredby(PG_FUNCTION_ARGS)
}
}
/*
- * Short-circuit 2: if geom1 is a point and geom2 is a polygon
- * call the point-in-polygon function.
+ * Short-circuit 2: if geom2 is polygonal and geom1 is a point
+ * or point-only collection, use the IntervalTree PIP fast path.
*/
- if (is_point(geom1) && is_poly(geom2))
+ if (is_point_or_collection(geom1) && is_poly(geom2) &&
+ try_itree_pointlike_pip(fcinfo, geom1, shared_geom2, itree_pip_covers, &pip_result))
{
- const GSERIALIZED *gpoint = shared_gserialized_get(shared_geom1);
- IntervalTree *itree = GetIntervalTree(fcinfo, shared_geom2);
- LWGEOM *lwpt = lwgeom_from_gserialized(gpoint);
- bool result = itree_pip_covers(itree, lwpt);
- lwgeom_free(lwpt);
- PG_RETURN_BOOL(result);
+ PG_RETURN_BOOL(pip_result);
}
initGEOS(lwpgnotice, lwgeom_geos_error);
diff --git a/regress/core/tickets.sql b/regress/core/tickets.sql
index 3e0533a76..413a1b9f1 100644
--- a/regress/core/tickets.sql
+++ b/regress/core/tickets.sql
@@ -1243,6 +1243,72 @@ SELECT 'equals212', ST_equals('POLYGON((0 0, 0 10, 10 10, 10 0, 0 0))'::geometry
SELECT 'equals213', ST_equals('POLYGON((0 0, 0 10, 10 10, 10 0, 0 0))'::geometry,
'GEOMETRYCOLLECTION (POLYGON((0 0, 10 0, 10 10, 0 10, 0 0)),MULTIPOINT(0 2, 5 5))'::geometry);
+-- #4749 point-only GeometryCollections in polygon predicates use the PIP path
+WITH g AS (
+ SELECT
+ 'POLYGON((0 0,0 10,10 10,10 0,0 0))'::geometry AS poly,
+ 'GEOMETRYCOLLECTION(GEOMETRYCOLLECTION(POINT(5 5)),MULTIPOINT((7 7),(8 8)))'::geometry AS pts_inside,
+ 'GEOMETRYCOLLECTION(POINT(5 5),POINT(0 0))'::geometry AS pts_boundary,
+ 'GEOMETRYCOLLECTION(POINT(5 5),POINT(11 11))'::geometry AS pts_outside
+)
+SELECT '#4749.contains',
+ ST_Contains(poly, pts_inside),
+ ST_Contains(poly, pts_boundary),
+ ST_Contains(poly, pts_outside)
+FROM g;
+
+WITH g AS (
+ SELECT
+ 'POLYGON((0 0,0 10,10 10,10 0,0 0))'::geometry AS poly,
+ 'GEOMETRYCOLLECTION(GEOMETRYCOLLECTION(POINT(5 5)),MULTIPOINT((7 7),(8 8)))'::geometry AS pts_inside,
+ 'GEOMETRYCOLLECTION(POINT(5 5),POINT(0 0))'::geometry AS pts_boundary,
+ 'GEOMETRYCOLLECTION(POINT(5 5),POINT(11 11))'::geometry AS pts_outside
+)
+SELECT '#4749.within',
+ ST_Within(pts_inside, poly),
+ ST_Within(pts_boundary, poly),
+ ST_Within(pts_outside, poly)
+FROM g;
+
+WITH g AS (
+ SELECT
+ 'POLYGON((0 0,0 10,10 10,10 0,0 0))'::geometry AS poly,
+ 'GEOMETRYCOLLECTION(GEOMETRYCOLLECTION(POINT(5 5)),MULTIPOINT((7 7),(8 8)))'::geometry AS pts_inside,
+ 'GEOMETRYCOLLECTION(POINT(5 5),POINT(0 0))'::geometry AS pts_boundary,
+ 'GEOMETRYCOLLECTION(POINT(5 5),POINT(11 11))'::geometry AS pts_outside
+)
+SELECT '#4749.covers',
+ ST_Covers(poly, pts_inside),
+ ST_Covers(poly, pts_boundary),
+ ST_Covers(poly, pts_outside)
+FROM g;
+
+WITH g AS (
+ SELECT
+ 'POLYGON((0 0,0 10,10 10,10 0,0 0))'::geometry AS poly,
+ 'GEOMETRYCOLLECTION(GEOMETRYCOLLECTION(POINT(5 5)),MULTIPOINT((7 7),(8 8)))'::geometry AS pts_inside,
+ 'GEOMETRYCOLLECTION(POINT(5 5),POINT(0 0))'::geometry AS pts_boundary,
+ 'GEOMETRYCOLLECTION(POINT(5 5),POINT(11 11))'::geometry AS pts_outside
+)
+SELECT '#4749.coveredby',
+ ST_CoveredBy(pts_inside, poly),
+ ST_CoveredBy(pts_boundary, poly),
+ ST_CoveredBy(pts_outside, poly)
+FROM g;
+
+WITH g AS (
+ SELECT
+ 'POLYGON((0 0,0 10,10 10,10 0,0 0))'::geometry AS poly,
+ 'GEOMETRYCOLLECTION(GEOMETRYCOLLECTION(POINT(5 5)),MULTIPOINT((7 7),(8 8)))'::geometry AS pts_inside,
+ 'GEOMETRYCOLLECTION(POINT(5 5),POINT(0 0))'::geometry AS pts_boundary,
+ 'GEOMETRYCOLLECTION(POINT(5 5),POINT(11 11))'::geometry AS pts_outside
+)
+SELECT '#4749.intersects',
+ ST_Intersects(poly, pts_inside),
+ ST_Intersects(pts_boundary, poly),
+ ST_Intersects(poly, pts_outside)
+FROM g;
+
-- #4299
SELECT '#4299', ST_Disjoint(ST_GeneratePoints(g, 1000), ST_GeneratePoints(g, 1000))
FROM (SELECT 'POLYGON((0 0,1 0,1 1,0 1,0 0))'::geometry AS g) AS f;
diff --git a/regress/core/tickets_expected b/regress/core/tickets_expected
index 9c3451edd..dc67b09d2 100644
--- a/regress/core/tickets_expected
+++ b/regress/core/tickets_expected
@@ -383,6 +383,11 @@ equals210|t
equals211|t
equals212|t
equals213|t
+#4749.contains|t|t|f
+#4749.within|t|t|f
+#4749.covers|t|t|f
+#4749.coveredby|t|t|f
+#4749.intersects|t|t|t
#4299|t
#4304|t|t|t
ERROR: BOX3D_construct: args can not be empty points
-----------------------------------------------------------------------
Summary of changes:
NEWS | 2 +
postgis/lwgeom_geos_predicates.c | 169 +++++++++++++++++++++++++++------------
regress/core/tickets.sql | 66 +++++++++++++++
regress/core/tickets_expected | 5 ++
4 files changed, 190 insertions(+), 52 deletions(-)
hooks/post-receive
--
PostGIS
More information about the postgis-tickets
mailing list