Skip to content

Does Simplification Mean the Same Thing Everywhere?

Eight backends, one dataset, four tolerances. They do not agree, and the options that promise to preserve topology, meaning the way shapes fit together, mostly preserve something narrower than that.

Every guide to shrinking a GeoJSON file says to simplify it, and then names a tolerance. This page asks whether that instruction means anything, by handing the same 548,471 positions to 8 implementations at the same tolerance and measuring what came back.

The dataset is Natural Earth 1:10m Admin 0 Countries, which is public domain. It has 258 features and, importantly, 344 pairs of countries that share a land border. That last number is what makes the dataset worth using: it has topology, meaning the shapes fit together along shared edges, and topology is the thing simplification quietly breaks.

The source borders are clean, which is what makes the rest measurable

Before simplifying anything, all 344 shared borders in the source touch exactly: not one pair overlaps, and the total overlapping area is 0 square degrees.

That baseline is the whole basis for the numbers below. Counting overlaps after simplification only measures damage if there was none to start with. Had the source shipped sloppy borders, most of what follows would be a property of the data itself, and the page would be worthless.

At the same tolerance, at 0.01 degrees

That is roughly 1,113 metres at the equator, a tolerance reasonable for a world map.

BackendPositions keptShape changedBorders brokenBorders overlappingSame twice
Shapelypreserve_topology=True190,479 (35%)0.1665%3262 of 344yes
Shapelypreserve_topology=False188,037 (34%)0.1672%3259 of 340yes
PostGISST_SimplifyPreserveTopology190,479 (35%)0.1665%3263 of 344yes
PostGISST_Simplify189,490 (35%)0.083%3247 of 340yes
Turf.jshighQuality191,168 (35%)0.0829%3246 of 344yes
GDALogr2ogr -simplify190,476 (35%)0.1665%3262 of 344no
mapshaperDouglas-Peucker, shared arcs169,036 (31%)0.1969%00 of 343yes
mapshaperVisvalingam, shared arcs206,338 (38%)0.1251%00 of 343yes

The position counts land within a few percent of each other, so at first glance the implementations agree. mapshaper kept 206,338 and mapshaper kept 169,036. That is the part everyone measures, and it is not where the difference is.

The column that matters is the last one

263 of the 344 shared borders now overlap under the worst backend at this tolerance, in data where none overlapped before. Two countries claim the same ground, along a border that used to be a single shared line. Nothing in the file is invalid, no validator complains, and the map looks fine until someone zooms in or runs a spatial join.

The cause is simple once stated. Each polygon is simplified on its own. A border between two countries is therefore simplified twice, once from each side, and the two results have no reason to agree. One side keeps a vertex the other discards, and the gap between them is a sliver.

Turning on the topology-preserving option does not fix this. Shapely with preserve_topology=True leaves 262 of 344 borders overlapping, which is 76%. With it switched off, 259 of 340, which is 76%. The denominators differ because simplifying without the option collapses a few small features entirely, so there are fewer pairs left to check. The share is the same either way, and the same holds for ST_SimplifyPreserveTopology against ST_Simplify.

That option is not broken and it is not lying. It preserves the topology of each geometry on its own, which means the polygon it hands back will not be self-intersecting or collapsed. It says nothing about the relationship between one polygon and its neighbour, because at the moment it runs, it has never seen the neighbour. The name invites a reading it was never offering.

One backend does not give the same answer twice

Every backend was run twice at every tolerance and the geometry it produced was hashed both times. GDAL returned different geometry on the second run at 0.01 degrees and 0.05 degrees. Identical input, identical command, two different answers.

Checked again in isolation, outside the benchmark: five runs of the same ogr2ogr -simplify over the same file produced 190,476 positions three times and 190,479 twice, with different file hashes. It comes from the tool itself, and it appears only at the larger tolerances tested.

The practical consequence is narrow but real. If you simplify as part of a build and expect the output to be byte-stable, it will not always be, and a content hash computed over it will move without your data changing.

What actually preserves shared borders

mapshaper are the only backends here that finish with every shared border intact, at every tolerance tested.

The reason is structural. mapshaper converts the collection into a topology first, so a border between two countries exists once as a single shared arc, in place of two independent outlines. Simplifying that arc changes both countries identically, because it is one line. The algorithm on top of it, Douglas-Peucker or Visvalingam, makes no difference to this: both come through clean.

It is the same idea TopoJSON is built on, and it is the reason that format exists. If your data is a coverage, which is to say adjacent polygons that tile an area, this is the property you need and plain GeoJSON simplification cannot give it to you.

Every tolerance

Damage grows with tolerance, and it starts earlier than people expect.

Backend0.001°0.005°0.01°0.05°
Shapelypreserve_topology=True10932% of borders24672% of borders26577% of borders29084% of borders
Shapelypreserve_topology=False10932% of borders24672% of borders26277% of borders28585% of borders
PostGISST_SimplifyPreserveTopology11032% of borders24772% of borders26677% of borders29185% of borders
PostGISST_Simplify10731% of borders23569% of borders25074% of borders25074% of borders
Turf.jshighQuality10631% of borders23468% of borders24972% of borders25474% of borders
GDALogr2ogr -simplify10932% of borders24672% of borders26577% of borders29084% of borders
mapshaperDouglas-Peucker, shared arcs00% of borders00% of borders00% of borders00% of borders
mapshaperVisvalingam, shared arcs00% of borders00% of borders00% of borders10% of borders

Each cell counts the shared borders that came apart or now overlap, out of 344. At the gentlest tolerance tested, about 111 metres, the independent backends are already damaging roughly a third of the borders in the dataset.

What to do about it

  • Isolated shapes. Points of interest, routes, buildings that do not touch. Any backend here is fine, and the choice comes down to what is already in your stack.
  • A coverage. Administrative boundaries, land use, census tracts, anything where polygons share edges. Use a topology-aware tool, or accept slivers along every shared border.
  • Do not rely on the preserve-topology flag for this. It is worth switching on for its own reasons, since it stops individual geometries collapsing, but it does not address shared borders and the numbers above are how much it does not.
  • Check afterwards. Overlapping polygons are valid GeoJSON, so nothing will tell you. A pairwise intersection test over neighbours is a few lines and is the only thing that catches it.

Method

Every figure here was produced by one script, run against a pinned copy of Natural Earth 1:10m Admin 0 Countries. Versions: shapely 2.1.2, postgis 3.4.2, mapshaper 0.7.61, gdal 3.8.4, turf 7.3.4.

A border counts as broken when two features that intersected in the source no longer intersect, and as overlapping when their interiors share area. Shape change is the area of the symmetric difference between each feature and its simplification, as a share of the original area. Hausdorff distance is recorded too, but only for geometries under five thousand positions, because it is quadratic and one large country takes the best part of a minute.

mapshaper takes its tolerance as a ground distance in metres, so the degree tolerance is converted at the equator. That conversion is approximate away from the equator, which is a limitation of comparing these tools at all, and not of the measurement: the tolerance is not a shared unit, which is part of the answer to the question in the title.

The data is published under CC BY 4.0 on the datasets page, and the file size guide uses the same dataset to work through what each optimisation is worth.