ADR 0013 — A GeometryCollection is flattened by the rules, and refused by the cadastre

Status: accepted · Amends: — · Amended by: —

Context

plugin-topology's isPolygonal was a two-line type test:

export function isPolygonal(feature: BlaeuFeature): boolean {
  return feature.geometry.type === 'Polygon' || feature.geometry.type === 'MultiPolygon'
}

It is not a two-line concern. That predicate is the appliesTo for all seven topology rules, the stored-neighbour filter in candidateNeighbours, and the co-committed batch filter in batchInCollection. So a geometry it rejects is not merely unchecked — it also becomes invisible as a neighbour, which switches off the relational rules for the honest parcels around it.

A GeometryCollection is rejected by that test, and a GeometryCollection is not exotic: it is what ArcGIS, several DXF converters, and some municipal exports produce when a parcel's rings travel together. Measured against a real cadastrePreset({ crs: 'EPSG:5254' }), two parcels overlapping by 1 199.6 m² were correctly refused as Polygons, and committed {ok: true} with zero issues when one was wrapped in a collection. A collection seeded alone then blinded a plainly-overlapping correct Polygon committed afterwards.

Nothing upstream stops it, and that is deliberate rather than accidental. Core supports GeometryCollection on purpose: normaliseGeometry recurses into its members (including nested ones), CrsService.area and .length sum them, FakeRenderer draws them, and plugin-edit and plugin-snap each have an explicit arm for them. No ADR mentioned the type, and no test in the repository named it.

Decision

Three tiers, three different answers, and the tier boundary is the whole point.

Core — unchanged. A GeometryCollection is legitimate RFC 7946 and the kernel keeps storing it. The kernel has never heard of a parcel, and "this shape is not a parcel" is not a kernel opinion.

plugin-topology — flatten, don't skip. A new polygonalGeometry(geometry) returns the polygonal content of a geometry — a Polygon or MultiPolygon unchanged, a collection reduced to a MultiPolygon of every polygonal member found recursively, and undefined when there is none. isPolygonal, polygonRings and polygonParts are all defined in terms of it, and so is prepare(), the single conversion boundary into JSTS.

Putting the flattening in prepare() — the single conversion boundary into JSTS — is what makes this safe rather than merely broader. Flattening is also the semantically correct reading: the union of a collection's polygons is the ground it claims.

"Any polygonal member" rather than "all": non-areal members are ignored rather than being grounds for refusal.

preset-cadastre — refuse it, as an error. Measurable is not the same as storable. A parcel whose geometry is a collection has no single boundary, so sınırlandırma is undefined for it, and the vertex tool cannot edit it at all — core's eachVertex deliberately declines to address a collection's members, because VertexRef has no way to name one. Storing one produces a parcel that looks fine on screen and is a dead end.

So parcelGeometryTypeRule reports a non-Polygon/MultiPolygon parcel as an error, scoped to the parcel collection. Error rather than warning because the fix is mechanical — flatten to a MultiPolygon on import — and the message says so, naming the offending type.

deriveAreaMiddleware widened its own copy of the predicate to match. That matters even though the cadastre now refuses to store a collection, because the middleware runs ahead of validation in the pipeline, it is exported and usable standalone, and a host that lowers the geometry-type rule to a warning must not end up with a stored parcel carrying no yüzölçümü.

Alternatives rejected

Reject a GeometryCollection at normaliseGeometry, so nothing downstream ever sees one. The tempting fix, and the cheapest: one guard, one error, seven rules and both neighbour filters correct by construction. Rejected because it puts a plugin's limitation into the kernel, in direct tension with invariant 1, and because being honest about it would mean deleting deliberate collection support from five other files — normaliseGeometry's recursion, CrsService.area and .length, FakeRenderer, and the explicit arms in plugin-edit and plugin-snap. A preset that cannot store a shape is not evidence that the kernel cannot.

Widen the predicate without flattening in prepare(). Two characters in isPolygonal, and the rules would start seeing collections. Rejected because JTS raises IllegalArgumentException: This method does not support GeometryCollection arguments from every overlay operation, so a collection reaching intersection() would trade a silent miss for a crash — a worse failure at a worse moment, since it lands in the middle of a commit rather than on import.

Require all members to be polygonal, rather than any. Stricter, and defensible on the grounds that a mixed collection is probably a conversion artefact. Rejected because a collection carrying a parcel plus its address point is still a parcel, and refusing to check its geometry because it also carries a point is the same silent miss by another route. Non-areal members are ignored, not grounds for refusal.

Consequences

A collection can no longer exempt itself, or its neighbours, from any topology rule. The seven rules and both neighbour filters see its polygons.

The cadastre preset refuses to store a collection parcel, which is a behaviour change for any host importing GC-wrapped parcels: those commits now fail with a cadastre.geometryType error instead of silently storing an unvalidated, uneditable parcel. That is the intended trade — the previous behaviour was the defect — and the remedy is one flattening pass at the import boundary. Hosts that genuinely want them can rebuild the rule list; parcelGeometryTypeRule and PARCEL_GEOMETRY_RULE_ID are exported for exactly that.

preset-urban and any third-party consumer of plugin-topology get the rule coverage without the cadastre's refusal, which is the tiering working as intended.

What this ADR does not do: teach the vertex tool to edit a collection's members. That needs an addressing scheme VertexRef does not have, and inventing one here would leak it into every plugin that consumes a ref. A collection remains storable-but-uneditable in the kernel, and the cadastre's error is the honest way to say so.

Tests

Both suites were confirmed to fail against the pre-fix predicate.

Edit this page on GitHub — this site is generated from docs/adr/0013-geometrycollection-is-flattened-not-rejected.md, which is the source of truth.