CoverageValidator::validate() loops over targets and calls the static CoveragePolygonValidator::validate(target, nearGeoms, gapWidth), which calls toCoveragePolygons(adjPolygons), constructing a fresh CoveragePolygon for every adjacent polygon of every target. CoveragePolygon lazily caches an IndexedPointInAreaLocator, but the wrapper itself never survives to the next target, so each polygon's point-in-area index is rebuilt once per adjacent target rather than once per validation run.
On a coverage with one large polygon adjacent to nearly everything, this dominates. Profiling coverage_is_valid on a real coastal coverage of 88,997 polygons / 20.7M vertices whose largest part (4.26M vertices) has an envelope intersecting 88,697 of the others (macOS sample, 11,230 samples, GEOS 3.13.1):
CoverageValidator::validate()
└ CoveragePolygonValidator::validate() 10,598
└ checkTargetRings → markInvalidInteriorSegments 8,905
└ markInvalidInteriorSection
└ CoveragePolygon::contains(CoordinateXY) 8,905
└ IndexedPointInAreaLocator::locate() 7,694 (68.5% of total)
└ TemplateSTRtree<SegmentView>::build() 7,694
└ createParentNodes → std::__introsort 7,159
Scaling, measured with geosop coverageValidate on main (a03789b), fixtures of N real parts plus the one large polygon:
| parts |
wall |
| 1,001 |
7.97 s |
| 4,001 |
31.07 s |
| 16,001 |
134.9 s |
That is near-linear in part count (≈N^1.06), 8.4 ms per part at 16k. Extrapolated to the full 88,997-part coverage: a floor around 850–900 s, several times the CoverageUnion (194 s) the validation guards. The sensitivity to a single member is stark: adding the one large polygon to a 1,000-part subset moves coverage_is_valid from 0.030 s to 39.8 s.
The relevant code is unchanged between 3.13.1 and current main, and JTS shares the design (CoverageValidator.java constructs a fresh CoveragePolygonValidator per target and caches nothing across targets).
Evidence: https://github.com/openwatersio/geos/tree/coverage-validator-cache-diagnostic (commit defa895)
CoverageValidator::validate()loops over targets and calls the staticCoveragePolygonValidator::validate(target, nearGeoms, gapWidth), which callstoCoveragePolygons(adjPolygons), constructing a freshCoveragePolygonfor every adjacent polygon of every target.CoveragePolygonlazily caches anIndexedPointInAreaLocator, but the wrapper itself never survives to the next target, so each polygon's point-in-area index is rebuilt once per adjacent target rather than once per validation run.On a coverage with one large polygon adjacent to nearly everything, this dominates. Profiling
coverage_is_validon a real coastal coverage of 88,997 polygons / 20.7M vertices whose largest part (4.26M vertices) has an envelope intersecting 88,697 of the others (macOSsample, 11,230 samples, GEOS 3.13.1):Scaling, measured with
geosop coverageValidateonmain(a03789b), fixtures of N real parts plus the one large polygon:That is near-linear in part count (≈N^1.06), 8.4 ms per part at 16k. Extrapolated to the full 88,997-part coverage: a floor around 850–900 s, several times the
CoverageUnion(194 s) the validation guards. The sensitivity to a single member is stark: adding the one large polygon to a 1,000-part subset movescoverage_is_validfrom 0.030 s to 39.8 s.The relevant code is unchanged between 3.13.1 and current
main, and JTS shares the design (CoverageValidator.javaconstructs a freshCoveragePolygonValidatorper target and caches nothing across targets).Evidence: https://github.com/openwatersio/geos/tree/coverage-validator-cache-diagnostic (commit defa895)