[SCM] PostGIS branch master updated. 3.6.0rc2-330-g5fac7d16a

git at osgeo.org git at osgeo.org
Fri Feb 6 08:43:19 PST 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  5fac7d16ab444240d465a36d717b4f0b95c068dd (commit)
      from  cbc0c3ae7bc093ede5451d64f00486b4d02f0c08 (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 5fac7d16ab444240d465a36d717b4f0b95c068dd
Author: Paul Ramsey <pramsey at cleverelephant.ca>
Date:   Fri Feb 6 08:41:18 2026 -0800

    Add ST_ClusterRelateWin, a souped up version of ST_ClusterIntersectingWin,
    that allows any DE9IM matrix to be used as the clustering condition.
    So only things that touch get added to the cluster, or only things
    with very specific kinds of overlap. (OK, probably 90% of
    usage will be things like touch, or ends-only-join for lines.)

diff --git a/doc/html/images/Makefile.in b/doc/html/images/Makefile.in
index cb01b2d60..203142611 100644
--- a/doc/html/images/Makefile.in
+++ b/doc/html/images/Makefile.in
@@ -80,6 +80,7 @@ GENERATED_IMAGES= \
 	st_closestpoint01.png \
 	st_closestpoint02.png \
 	st_clusterkmeans02.png \
+	st_clusterrelatewin01.png \
 	st_centroid01.png \
 	st_centroid02.png \
 	st_centroid03.png \
diff --git a/doc/html/images/wkt/st_clusterrelatewin01.wkt b/doc/html/images/wkt/st_clusterrelatewin01.wkt
new file mode 100644
index 000000000..a5964ed0b
--- /dev/null
+++ b/doc/html/images/wkt/st_clusterrelatewin01.wkt
@@ -0,0 +1,6 @@
+ArgA;LINESTRING(2 2,50 50)
+ArgA;LINESTRING(98 98,50 50)
+ArgA;LINESTRING(2 50,50 2)
+ArgA;LINESTRING(2 50,2 194)
+ArgA;LINESTRING(98 98,98 194)
+ArgA;LINESTRING(74 122,122 170)
diff --git a/doc/reference_cluster.xml b/doc/reference_cluster.xml
index 5b2a35a65..dfe7e90eb 100644
--- a/doc/reference_cluster.xml
+++ b/doc/reference_cluster.xml
@@ -281,6 +281,89 @@ FROM testdata;
     </refentry>
 
 
+
+    <refentry xml:id="ST_ClusterRelateWin">
+      <refnamediv>
+        <refname>ST_ClusterRelateWin</refname>
+
+        <refpurpose>Window function that returns a cluster id for each input geometry, clustering input geometries into connected sets using the relate pattern to determine whether the geometries are connected.</refpurpose>
+      </refnamediv>
+
+      <refsynopsisdiv>
+        <funcsynopsis>
+          <funcprototype>
+            <funcdef>integer <function>ST_ClusterRelateWin</function></funcdef>
+            <paramdef><type>geometry winset </type> <parameter>geom</parameter></paramdef>
+            <paramdef><type>text </type> <parameter>relate_matrix</parameter></paramdef>
+          </funcprototype>
+        </funcsynopsis>
+      </refsynopsisdiv>
+
+      <refsection>
+        <title>Description</title>
+
+        <para>A window function that builds connected clusters of geometries that intersect. Geometries are added to a cluster if they share a pairwise DE9IM relationship with another member of the cluster. With this function it is possible to build a cluster of all objects that touch at boundaries, but exclude those that merely overlap.</para>
+
+        <para role="availability" conformance="3.7.0">Availability: 3.7.0</para>
+      </refsection>
+
+      <refsection>
+        <title>Examples</title>
+        <para>This collection of line strings would form a single cluster using <xref linkend="ST_ClusterIntersectingWin"/>, but using <xref linkend="ST_ClusterRelateWin"/> can be clustered into three groups that connect only via their end points.</para>
+
+        <para>
+          <informalfigure>
+            <mediaobject>
+                <imageobject>
+                <imagedata fileref="images/st_clusterrelatewin01.png"/>
+                </imageobject>
+                <caption><para>Connected and overlapping linestrings</para></caption>
+            </mediaobject>
+          </informalfigure>
+        </para>
+
+        <programlisting>
+CREATE TABLE clusterrelate (
+  id serial,
+  geom geometry);
+
+INSERT INTO clusterrelate (geom)
+  VALUES
+  ('LINESTRING(0 0,1 1)'),
+  ('LINESTRING(2 2,1 1)'),
+  ('LINESTRING(0 1,1 0)'),
+  ('LINESTRING(0 1,0 4)'),
+  ('LINESTRING(2 2,2 4)'),
+  ('LINESTRING(1.5 2.5,2.5 3.5)');
+
+SELECT id,
+  ST_AsText(geom),
+  ST_ClusterRelateWin(geom, '****0****') OVER () AS cluster
+FROM clusterrelate;
+
+ id |          st_astext          | cluster
+----+-----------------------------+---------
+  1 | LINESTRING(0 0,1 1)         |       0
+  2 | LINESTRING(2 2,1 1)         |       0
+  3 | LINESTRING(0 1,1 0)         |       1
+  4 | LINESTRING(0 1,0 4)         |       1
+  5 | LINESTRING(2 2,2 4)         |       0
+  6 | LINESTRING(1.5 2.5,2.5 3.5) |       2
+        </programlisting>
+      </refsection>
+      <refsection>
+        <title>See Also</title>
+        <para>
+            <xref linkend="ST_Relate"/>,
+            <xref linkend="ST_ClusterIntersectingWin"/>,
+            <xref linkend="ST_ClusterWithinWin"/>
+        </para>
+      </refsection>
+
+    </refentry>
+
+
+
 	<refentry xml:id="ST_ClusterKMeans">
 	  <refnamediv>
 		<refname>ST_ClusterKMeans</refname>
diff --git a/liblwgeom/lwgeom_geos.h b/liblwgeom/lwgeom_geos.h
index 84638d82b..2d80819fb 100644
--- a/liblwgeom/lwgeom_geos.h
+++ b/liblwgeom/lwgeom_geos.h
@@ -44,6 +44,7 @@ GEOSGeometry* make_geos_segment(double x1, double y1, double x2, double y2);
 
 int cluster_intersecting(GEOSGeometry **geoms, uint32_t num_geoms, GEOSGeometry ***clusterGeoms, uint32_t *num_clusters);
 int union_intersecting_pairs(GEOSGeometry** geoms, uint32_t num_geoms, UNIONFIND* uf);
+int union_related_pairs(GEOSGeometry** geoms, uint32_t num_geoms, const char* im, UNIONFIND* uf);
 int cluster_within_distance(LWGEOM **geoms, uint32_t num_geoms, double tolerance, LWGEOM ***clusterGeoms, uint32_t *num_clusters);
 int union_dbscan(LWGEOM **geoms, uint32_t num_geoms, UNIONFIND *uf, double eps, uint32_t min_points, char **is_in_cluster_ret);
 
diff --git a/liblwgeom/lwgeom_geos_cluster.c b/liblwgeom/lwgeom_geos_cluster.c
index c0e8b2f91..318d3558e 100644
--- a/liblwgeom/lwgeom_geos_cluster.c
+++ b/liblwgeom/lwgeom_geos_cluster.c
@@ -239,6 +239,100 @@ union_intersecting_pairs(GEOSGeometry** geoms, uint32_t num_geoms, UNIONFIND* uf
 	return success;
 }
 
+
+
+/*
+ * Identify geometries that match the relate pattern
+ * and mark them as being in the same set
+ */
+int
+union_related_pairs(
+	GEOSGeometry** geoms,
+	uint32_t num_geoms,
+	const char* matrix,
+	UNIONFIND* uf)
+{
+#if POSTGIS_GEOS_VERSION >= 31300
+	int success = LW_SUCCESS;
+	uint32_t p, i;
+	struct STRTree tree;
+	struct QueryContext cxt =
+	{
+		.items_found = NULL,
+		.num_items_found = 0,
+		.items_found_size = 0
+	};
+
+	if (num_geoms <= 1)
+		return LW_SUCCESS;
+
+	tree = make_strtree((void**) geoms, num_geoms, LW_FALSE);
+	if (tree.tree == NULL)
+	{
+		destroy_strtree(&tree);
+		return LW_FAILURE;
+	}
+
+	for (p = 0; p < num_geoms; p++)
+	{
+		const GEOSPreparedGeometry* prep = NULL;
+
+		if (!geoms[p] || GEOSisEmpty(geoms[p]))
+			continue;
+
+		cxt.num_items_found = 0;
+		GEOSSTRtree_query(tree.tree, geoms[p], &query_accumulate, &cxt);
+
+		/*
+		 * II BI EI
+		 * IB BB EB
+		 * IE BE EE
+		 */
+		for (i = 0; i < cxt.num_items_found; i++)
+		{
+			uint32_t q = *((uint32_t*) cxt.items_found[i]);
+
+			if (p != q && UF_find(uf, p) != UF_find(uf, q))
+			{
+				int geos_result;
+
+				if (prep == NULL)
+				{
+					prep = GEOSPrepare(geoms[p]);
+				}
+				geos_result = GEOSPreparedRelatePattern(prep, geoms[q], matrix);
+
+				if (geos_result > 1)
+				{
+					success = LW_FAILURE;
+					break;
+				}
+				else if (geos_result)
+				{
+					UF_union(uf, p, q);
+				}
+			}
+		}
+
+		if (prep)
+			GEOSPreparedGeom_destroy(prep);
+
+		if (!success)
+			break;
+	}
+
+	if (cxt.items_found)
+		lwfree(cxt.items_found);
+
+	destroy_strtree(&tree);
+	return success;
+
+#else /* POSTGIS_GEOS_VERSION >= 31300 */
+	return LW_FAILURE;
+#endif
+}
+
+
 /** Takes an array of GEOSGeometry* and constructs an array of GEOSGeometry*, where each element in the constructed
  *  array is a GeometryCollection representing a set of interconnected geometries. Caller is responsible for
  *  freeing the input array, but not for destroying the GEOSGeometry* items inside it.  */
diff --git a/liblwgeom/lwunionfind.h b/liblwgeom/lwunionfind.h
index 34b2efd0e..75e0ae788 100644
--- a/liblwgeom/lwunionfind.h
+++ b/liblwgeom/lwunionfind.h
@@ -22,9 +22,7 @@
  *
  **********************************************************************/
 
-
-#ifndef _LWUNIONFIND
-#define _LWUNIONFIND 1
+#pragma once
 
 #include "liblwgeom.h"
 
@@ -61,4 +59,3 @@ uint32_t* UF_ordered_by_cluster(UNIONFIND* uf);
  * */
 uint32_t* UF_get_collapsed_cluster_ids(UNIONFIND* uf, const char* is_in_cluster);
 
-#endif
diff --git a/postgis/lwgeom_geos.c b/postgis/lwgeom_geos.c
index 339214f0d..e4aacfcfe 100644
--- a/postgis/lwgeom_geos.c
+++ b/postgis/lwgeom_geos.c
@@ -2031,7 +2031,8 @@ Datum clusterintersecting_garray(PG_FUNCTION_ARGS)
 	ArrayType *array, *result;
 	int is3d = 0;
 	uint32 nelems, nclusters, i;
-	GEOSGeometry **geos_inputs, **geos_results;
+	GEOSGeometry **geos_inputs;
+	GEOSGeometry **geos_results;
 	int32_t srid = SRID_UNKNOWN;
 
 	/* Parameters used to construct a result array */
diff --git a/postgis/lwgeom_transform.c b/postgis/lwgeom_transform.c
index 58bbac8d6..4a23fe3fc 100644
--- a/postgis/lwgeom_transform.c
+++ b/postgis/lwgeom_transform.c
@@ -509,6 +509,7 @@ srs_find_planar(const char *auth_name, const LWGEOM *bounds, struct srs_data *st
 
 	while (crs_list && *crs_list)
 	{
+		uint32_t num_entries = state->num_entries;
 		/* Read current crs and move forward one entry */
 		PROJ_CRS_INFO *crs = *crs_list++;
 
@@ -524,10 +525,10 @@ srs_find_planar(const char *auth_name, const LWGEOM *bounds, struct srs_data *st
 		srs_state_memcheck(state);
 
 		/* Write the entry into the entry list and increment */
-		state->entries[state->num_entries].auth_name = cstring_to_text(crs->auth_name);
-		state->entries[state->num_entries].auth_code = cstring_to_text(crs->code);
-		state->entries[state->num_entries].sort = area;
-		state->num_entries++;
+		state->entries[num_entries].auth_name = cstring_to_text(crs->auth_name);
+		state->entries[num_entries].auth_code = cstring_to_text(crs->code);
+		state->entries[num_entries].sort = area;
+		state->num_entries = num_entries + 1;
 	}
 
 	/* Put the list of entries into order of area size, smallest to largest */
diff --git a/postgis/lwgeom_window.c b/postgis/lwgeom_window.c
index 90af7a867..4e931af53 100644
--- a/postgis/lwgeom_window.c
+++ b/postgis/lwgeom_window.c
@@ -350,6 +350,90 @@ Datum ST_ClusterIntersectingWin(PG_FUNCTION_ARGS)
 }
 
 
+extern Datum ST_ClusterRelateWin(PG_FUNCTION_ARGS);
+PG_FUNCTION_INFO_V1(ST_ClusterRelateWin);
+Datum ST_ClusterRelateWin(PG_FUNCTION_ARGS)
+{
+#if POSTGIS_GEOS_VERSION < 31300
+	lwpgerror("The GEOS version this PostGIS binary "
+	          "was compiled against (%d) doesn't support "
+	          "'ST_ClusterRelateWin' function (3.13.0+ required)",
+	          POSTGIS_GEOS_VERSION);
+	          PG_RETURN_NULL();
+#else
+
+	WindowObject win_obj = PG_WINDOW_OBJECT();
+	uint32_t row = WinGetCurrentPosition(win_obj);
+	uint32_t ngeoms = WinGetPartitionRowCount(win_obj);
+	cluster_context* context = fetch_cluster_context(win_obj, ngeoms);
+
+	if (row == 0) /* beginning of the partition; do all of the work now */
+	{
+		bool matrix_is_null = false;
+		uint32_t i;
+		uint32_t* result_ids;
+		GEOSGeometry** geoms = palloc0(ngeoms * sizeof(GEOSGeometry*));
+		UNIONFIND* uf = UF_create(ngeoms);
+		char *matrix;
+		text *txtIm = DatumGetTextP(WinGetFuncArgCurrent(win_obj, 1, &matrix_is_null));
+		if (matrix_is_null || VARSIZE_ANY_EXHDR(txtIm) != 9)
+		{
+			elog(ERROR,"Invalid relate matrix provided");
+			PG_RETURN_NULL();
+		}
+		matrix = text_to_cstring(txtIm);
+
+		context->is_error = LW_TRUE; /* until proven otherwise */
+		initGEOS(lwpgnotice, lwgeom_geos_error);
+
+		for (i = 0; i < ngeoms; i++)
+		{
+			bool geom_is_null;
+			geoms[i] = read_geos_from_partition(win_obj, i, &geom_is_null);
+			context->clusters[i].is_null = geom_is_null;
+
+			if (!geoms[i])
+			{
+				elog(ERROR, "Error reading geometry");
+				PG_RETURN_NULL();
+			}
+		}
+
+		if (union_related_pairs(geoms, ngeoms, matrix, uf) == LW_SUCCESS)
+			context->is_error = LW_FALSE;
+
+		for (i = 0; i < ngeoms; i++)
+		{
+			GEOSGeom_destroy(geoms[i]);
+		}
+		pfree(geoms);
+		pfree(matrix);
+
+		if (context->is_error)
+		{
+			UF_destroy(uf);
+			elog(ERROR, "Error during clustering");
+			PG_RETURN_NULL();
+		}
+
+		result_ids = UF_get_collapsed_cluster_ids(uf, NULL);
+		for (i = 0; i < ngeoms; i++)
+		{
+			context->clusters[i].cluster_id = result_ids[i];
+		}
+
+		pfree(result_ids);
+		UF_destroy(uf);
+	}
+
+	if (context->clusters[row].is_null)
+		PG_RETURN_NULL();
+
+	PG_RETURN_INT32(context->clusters[row].cluster_id);
+#endif
+}
+
+
 extern Datum ST_ClusterKMeans(PG_FUNCTION_ARGS);
 PG_FUNCTION_INFO_V1(ST_ClusterKMeans);
 Datum ST_ClusterKMeans(PG_FUNCTION_ARGS)
diff --git a/postgis/postgis.sql.in b/postgis/postgis.sql.in
index 4d13ced28..118a6499e 100644
--- a/postgis/postgis.sql.in
+++ b/postgis/postgis.sql.in
@@ -1941,6 +1941,13 @@ CREATE OR REPLACE FUNCTION ST_ClusterIntersectingWin(geometry)
 	LANGUAGE 'c' IMMUTABLE STRICT WINDOW PARALLEL SAFE
 	_COST_HIGH;
 
+-- Availability: 3.7.0
+CREATE OR REPLACE FUNCTION ST_ClusterRelateWin(geom geometry, relate_matrix text)
+	RETURNS int
+	AS 'MODULE_PATHNAME', 'ST_ClusterRelateWin'
+	LANGUAGE 'c' IMMUTABLE STRICT WINDOW PARALLEL SAFE
+	_COST_HIGH;
+
 -- Availability: 1.2.2
 CREATE OR REPLACE FUNCTION ST_LineMerge(geometry)
 	RETURNS geometry

-----------------------------------------------------------------------

Summary of changes:
 doc/html/images/Makefile.in                   |  1 +
 doc/html/images/wkt/st_clusterrelatewin01.wkt |  6 ++
 doc/reference_cluster.xml                     | 83 +++++++++++++++++++++++
 liblwgeom/lwgeom_geos.h                       |  1 +
 liblwgeom/lwgeom_geos_cluster.c               | 94 +++++++++++++++++++++++++++
 liblwgeom/lwunionfind.h                       |  5 +-
 postgis/lwgeom_geos.c                         |  3 +-
 postgis/lwgeom_transform.c                    |  9 +--
 postgis/lwgeom_window.c                       | 84 ++++++++++++++++++++++++
 postgis/postgis.sql.in                        |  7 ++
 10 files changed, 284 insertions(+), 9 deletions(-)
 create mode 100644 doc/html/images/wkt/st_clusterrelatewin01.wkt


hooks/post-receive
-- 
PostGIS


More information about the postgis-tickets mailing list