[PostGIS] #6098: ST_Collect can turn valid POLYGONs into invalid MULTIPOLYGONs

PostGIS trac at osgeo.org
Sat Jul 11 16:21:42 PDT 2026


#6098: ST_Collect can turn valid POLYGONs into invalid MULTIPOLYGONs
------------------------------------------------+--------------------------
 Reporter:  pnorman                             |      Owner:  pramsey
     Type:  defect                              |     Status:  new
 Priority:  medium                              |  Milestone:  PostGIS
                                                |  3.6.5
Component:  postgis                             |    Version:  3.6.x
 Keywords:  collect valid multipolygon invalid  |
------------------------------------------------+--------------------------
 If you have two valid POLYGONs that touch each other running ST_Collect on
 them can return an invalid MULTIPOLYGON.

 ST_Collect appears to make the assumption that any set of geometries of a
 homogeneous type can be turned into a Multi* of that type. This is true
 for points and linestrings, but not true for multipolygons. In the case of
 the former a Multi* adds no additional validity constraints but for the
 latter constraints are added that none of the member POLYGONs of the
 MULTIPOLYGON may touch.

 This can be seen with the following queries
 {{{
 SELECT ST_IsValid(geom), ST_IsValidReason(geom), ST_Area(geom),
 substring(st_astext(geom) FOR 40) FROM (SELECT geom geom FROM (VALUES
 ('SRID=3857;POLYGON((-9268999.088845689
 5214595.936014973,-9267812.06685146 5213205.196203408,-9267809.584426817
 5213197.55972637,-9267809.7848019 5213191.263779518,-9267785.83997943
 5212881.969248425,-9267744.930066563 5212353.6841178,-9267302.724521339
 5212418.611175549,-9267290.724280229 5212414.921270518,-9266682.975520242
 5212512.470794289,-9266579.793484228 5212522.109832203,-9266528.809157444
 5212523.133980559,-9266547.277060967 5212567.036914927,-9266554.24566109
 5212610.111689871,-9266659.954649547 5213035.237094378,-9266714.378748598
 5213312.84557948,-9266702.979632739 5213313.583630357,-9266745.025004411
 5213471.723654301,-9266767.077395536 5213559.146894767,-9266982.63645751
 5213557.429752363,-9266988.859217046 5213697.935206385,-9267005.278841939
 5214068.187909914,-9267249.101922622 5214061.1834017765,-9267259.031621201
 5214379.554829701,-9267016.98965237 5214383.652227536,-9267027.698587382
 5214642.3040552875,-9267929.620233739 5214629.454150022,-9267991.458210874
 5214629.258313381,-9268052.171861153 5214636.63985105,-9268099.404721096
 5214645.6935408255,-9268145.46872639 5214657.534153168,-9268189.796147622
 5214665.427902955,-9268312.715129355 5214658.52840373,-9268415.529811053
 5214628.49003122,-9268506.84518935 5214613.922810667,-9268986.108993063
 5214604.718508099,-9268999.088845689 5214595.936014973))'::geometry),
 ('SRID=3857;POLYGON((-9268999.088845689
 5214595.936014973,-9267812.06685146 5213205.196203408,-9267809.584426817
 5213197.55972637,-9267809.7848019 5213191.263779518,-9267744.930066563
 5212353.6841178,-9267302.724521339 5212418.611175549,-9267290.724280229
 5212414.921270518,-9266816.224950723 5212491.023970135,-9266682.975520242
 5212512.470794289,-9266579.793484228 5212522.109832203,-9266528.809157444
 5212523.133980559,-9266547.277060967 5212567.036914927,-9266554.24566109
 5212610.111689871,-9266659.954649547 5213035.237094378,-9266714.378748598
 5213312.84557948,-9266702.979632739 5213313.583630357,-9266745.025004411
 5213471.723654301,-9266767.077395536 5213559.146894767,-9266982.63645751
 5213557.429752363,-9267005.278841939 5214068.187909914,-9267249.101922622
 5214061.1834017765,-9267259.031621201 5214379.554829701,-9267016.98965237
 5214383.652227536,-9267027.698587382 5214642.3040552875,-9267929.620233739
 5214629.454150022,-9267991.458210874 5214629.258313381,-9268052.171861153
 5214636.63985105,-9268099.404721096 5214645.6935408255,-9268145.46872639
 5214657.534153168,-9268189.796147622 5214665.427902955,-9268312.715129355
 5214658.52840373,-9268365.569623584 5214642.891566027,-9268415.529811053
 5214628.49003122,-9268506.84518935 5214613.922810667,-9268986.108993063
 5214604.718508099,-9268999.088845689 5214595.936014973))'::geometry)
 ) AS t(geom)) AS q;

 SELECT ST_IsValid(geom), ST_IsValidReason(geom), ST_Area(geom),
 substring(st_astext(geom) FOR 40) FROM (SELECT ST_Collect(geom) geom FROM
 (VALUES
 ('SRID=3857;POLYGON((-9268999.088845689
 5214595.936014973,-9267812.06685146 5213205.196203408,-9267809.584426817
 5213197.55972637,-9267809.7848019 5213191.263779518,-9267785.83997943
 5212881.969248425,-9267744.930066563 5212353.6841178,-9267302.724521339
 5212418.611175549,-9267290.724280229 5212414.921270518,-9266682.975520242
 5212512.470794289,-9266579.793484228 5212522.109832203,-9266528.809157444
 5212523.133980559,-9266547.277060967 5212567.036914927,-9266554.24566109
 5212610.111689871,-9266659.954649547 5213035.237094378,-9266714.378748598
 5213312.84557948,-9266702.979632739 5213313.583630357,-9266745.025004411
 5213471.723654301,-9266767.077395536 5213559.146894767,-9266982.63645751
 5213557.429752363,-9266988.859217046 5213697.935206385,-9267005.278841939
 5214068.187909914,-9267249.101922622 5214061.1834017765,-9267259.031621201
 5214379.554829701,-9267016.98965237 5214383.652227536,-9267027.698587382
 5214642.3040552875,-9267929.620233739 5214629.454150022,-9267991.458210874
 5214629.258313381,-9268052.171861153 5214636.63985105,-9268099.404721096
 5214645.6935408255,-9268145.46872639 5214657.534153168,-9268189.796147622
 5214665.427902955,-9268312.715129355 5214658.52840373,-9268415.529811053
 5214628.49003122,-9268506.84518935 5214613.922810667,-9268986.108993063
 5214604.718508099,-9268999.088845689 5214595.936014973))'::geometry),
 ('SRID=3857;POLYGON((-9268999.088845689
 5214595.936014973,-9267812.06685146 5213205.196203408,-9267809.584426817
 5213197.55972637,-9267809.7848019 5213191.263779518,-9267744.930066563
 5212353.6841178,-9267302.724521339 5212418.611175549,-9267290.724280229
 5212414.921270518,-9266816.224950723 5212491.023970135,-9266682.975520242
 5212512.470794289,-9266579.793484228 5212522.109832203,-9266528.809157444
 5212523.133980559,-9266547.277060967 5212567.036914927,-9266554.24566109
 5212610.111689871,-9266659.954649547 5213035.237094378,-9266714.378748598
 5213312.84557948,-9266702.979632739 5213313.583630357,-9266745.025004411
 5213471.723654301,-9266767.077395536 5213559.146894767,-9266982.63645751
 5213557.429752363,-9267005.278841939 5214068.187909914,-9267249.101922622
 5214061.1834017765,-9267259.031621201 5214379.554829701,-9267016.98965237
 5214383.652227536,-9267027.698587382 5214642.3040552875,-9267929.620233739
 5214629.454150022,-9267991.458210874 5214629.258313381,-9268052.171861153
 5214636.63985105,-9268099.404721096 5214645.6935408255,-9268145.46872639
 5214657.534153168,-9268189.796147622 5214665.427902955,-9268312.715129355
 5214658.52840373,-9268365.569623584 5214642.891566027,-9268415.529811053
 5214628.49003122,-9268506.84518935 5214613.922810667,-9268986.108993063
 5214604.718508099,-9268999.088845689 5214595.936014973))'::geometry)
 ) AS t(geom)) AS q;
 }}}

 {{{
  st_isvalid | st_isvalidreason |      st_area      |
 substring
 ------------+------------------+-------------------+------------------------------------------
  t          | Valid Geometry   | 2935806.900413173 |
 POLYGON((-9268999.088845689 5214595.9360
  t          | Valid Geometry   | 2935811.528830427 |
 POLYGON((-9268999.088845689 5214595.9360
 (2 rows)

 NOTICE:  Self-intersection at or near point -9267016.9896523692
 5214383.6522275358
  st_isvalid |                   st_isvalidreason                    |
 st_area     |                substring
 ------------+-------------------------------------------------------+-----------------+------------------------------------------
  f          | Self-intersection[-9267016.98965237 5214383.65222754] |
 5871618.4292436 | MULTIPOLYGON(((-9268999.088845689 521459
 (1 row)
 }}}

 This is not a "harmless" invalidity as
 https://github.com/pnorman/spirit/issues/87 shows that with the right
 geometries and subsequent processing with ST_SimplifyPreserveTopology and
 ST_AsMVTGeom results in a null geometry

 A work-around appears to be to wrap each ST_Collect which may contain
 touching polygons with a ST_ForceCollection, turning the MULTIPOLYGON into
 a GEOMETRYCOLLECTION

 {{{
 SELECT ST_IsValid(geom), ST_IsValidReason(geom), ST_Area(geom),
 substring(st_astext(geom) FOR 40) FROM (SELECT
 ST_ForceCollection(ST_Collect(geom)) geom FROM (VALUES
 ('SRID=3857;POLYGON((-9268999.088845689
 5214595.936014973,-9267812.06685146 5213205.196203408,-9267809.584426817
 5213197.55972637,-9267809.7848019 5213191.263779518,-9267785.83997943
 5212881.969248425,-9267744.930066563 5212353.6841178,-9267302.724521339
 5212418.611175549,-9267290.724280229 5212414.921270518,-9266682.975520242
 5212512.470794289,-9266579.793484228 5212522.109832203,-9266528.809157444
 5212523.133980559,-9266547.277060967 5212567.036914927,-9266554.24566109
 5212610.111689871,-9266659.954649547 5213035.237094378,-9266714.378748598
 5213312.84557948,-9266702.979632739 5213313.583630357,-9266745.025004411
 5213471.723654301,-9266767.077395536 5213559.146894767,-9266982.63645751
 5213557.429752363,-9266988.859217046 5213697.935206385,-9267005.278841939
 5214068.187909914,-9267249.101922622 5214061.1834017765,-9267259.031621201
 5214379.554829701,-9267016.98965237 5214383.652227536,-9267027.698587382
 5214642.3040552875,-9267929.620233739 5214629.454150022,-9267991.458210874
 5214629.258313381,-9268052.171861153 5214636.63985105,-9268099.404721096
 5214645.6935408255,-9268145.46872639 5214657.534153168,-9268189.796147622
 5214665.427902955,-9268312.715129355 5214658.52840373,-9268415.529811053
 5214628.49003122,-9268506.84518935 5214613.922810667,-9268986.108993063
 5214604.718508099,-9268999.088845689 5214595.936014973))'::geometry),
 ('SRID=3857;POLYGON((-9268999.088845689
 5214595.936014973,-9267812.06685146 5213205.196203408,-9267809.584426817
 5213197.55972637,-9267809.7848019 5213191.263779518,-9267744.930066563
 5212353.6841178,-9267302.724521339 5212418.611175549,-9267290.724280229
 5212414.921270518,-9266816.224950723 5212491.023970135,-9266682.975520242
 5212512.470794289,-9266579.793484228 5212522.109832203,-9266528.809157444
 5212523.133980559,-9266547.277060967 5212567.036914927,-9266554.24566109
 5212610.111689871,-9266659.954649547 5213035.237094378,-9266714.378748598
 5213312.84557948,-9266702.979632739 5213313.583630357,-9266745.025004411
 5213471.723654301,-9266767.077395536 5213559.146894767,-9266982.63645751
 5213557.429752363,-9267005.278841939 5214068.187909914,-9267249.101922622
 5214061.1834017765,-9267259.031621201 5214379.554829701,-9267016.98965237
 5214383.652227536,-9267027.698587382 5214642.3040552875,-9267929.620233739
 5214629.454150022,-9267991.458210874 5214629.258313381,-9268052.171861153
 5214636.63985105,-9268099.404721096 5214645.6935408255,-9268145.46872639
 5214657.534153168,-9268189.796147622 5214665.427902955,-9268312.715129355
 5214658.52840373,-9268365.569623584 5214642.891566027,-9268415.529811053
 5214628.49003122,-9268506.84518935 5214613.922810667,-9268986.108993063
 5214604.718508099,-9268999.088845689 5214595.936014973))'::geometry)
 ) AS t(geom)) AS q;
 }}}
 {{{
  st_isvalid | st_isvalidreason |     st_area     |
 substring
 ------------+------------------+-----------------+------------------------------------------
  t          | Valid Geometry   | 5871618.4292436 |
 GEOMETRYCOLLECTION(POLYGON((-9268999.088
 }}}

 This technique is not satisfactory for a few reasons
 1. There's still an invalid geometry appearing in an intermediate stage
 2. No one will expect valid geometries to be invalid when collected
 3. There is no single aggregate function to form a geometry collection

 The reason for needing the multiple POLYGONs in one geometry is later on
 the output is passed to ST_AsMVT. MVT multipolygon geometries are a
 sequence of polygons encoded into a POLYGON geometry with multiple
 exterior/interior rings and all the validity constraints are on the
 individual exterior/interior sets in the sequence.
-- 
Ticket URL: <https://trac.osgeo.org/postgis/ticket/6098>
PostGIS <http://trac.osgeo.org/postgis/>
The PostGIS Trac is used for bug, enhancement & task tracking, a user and developer wiki, and a view into the subversion code repository of PostGIS project.


More information about the postgis-tickets mailing list