Repository: OSGeo/gdal
Pull request #12370: Add gdal vector geom simplify-coverage
Author: login: dbaston
Labels: name: gdal_cli
description: Anything related to the new 3.11 "gdal" CLI frontend
Created: 2025-05-13T14:57:14Z | Merged: 2025-05-18T14:49:22Z

Implements coverage simplification as described in #12300.

Some things on my mind:

I implemented this as a pipeline algorithm, although it cannot be performed in a streaming way.

I didn't see a way to use `GDALVectorAbstractAlgorithm` since that ultimately requires one-to-one or one-to-many feature translation. I didn't see a good way to extend it for many-to-many. Since I didn't use `GDALVectorAbstractAlgorithm`, I ended up duplicating bits of its functionality. I'd like to follow this up with a `clean-coverage` algorithm and figured I could generalize some of the new code then.

In the custom `GDALDataset` overload, I did not implement a few methods such as `GetFeature` by ID and dataset-level feature iteration. It's not clear to me if/when these would be called in a pipeline scenario. 

In an earlier version of this implementation, instead of creating a custom `GDALDataset` overload, I created a dataset with the memory driver and wrote the results there. This is much simpler, except that I didn't see a good way to create output layers with the same fields as the inputs. Is there something like `GDALDataset::CopyLayer` that copies only the layer definition, but not the contents?

I interacted with GEOS directly in the algorithm instead of first creating an `OGRGeometryCollection::SimplifyCoverage`. This allows the implementation to be a bit more memory-efficient (for example, clearing inputs as soon as they are not needed) but I can see an argument for containing all GEOS interfacing in one place.

It seems like this algorithm should implicitly perform a `PROMOTE_TO_MULTI` sort of behavior, but I haven't figured out a clean way to do that yet.


Diff:
diff --git a/apps/CMakeLists.txt b/apps/CMakeLists.txt
index f4381d5a1f05..1781ce3baff0 100644
--- a/apps/CMakeLists.txt
+++ b/apps/CMakeLists.txt
@@ -77,6 +77,7 @@ add_library(
   gdalalg_vector_geom_make_valid.cpp
   gdalalg_vector_geom_segmentize.cpp
   gdalalg_vector_geom_simplify.cpp
+  gdalalg_vector_geom_simplify_coverage.cpp
   gdalalg_vector_geom_buffer.cpp
   gdalalg_vector_geom_swap_xy.cpp
   gdalalg_vector_grid.cpp
diff --git a/apps/gdalalg_vector_geom.cpp b/apps/gdalalg_vector_geom.cpp
index b27f7f2223db..982518c9c41e 100644
--- a/apps/gdalalg_vector_geom.cpp
+++ b/apps/gdalalg_vector_geom.cpp
@@ -16,6 +16,7 @@
 #include "gdalalg_vector_geom_make_valid.h"
 #include "gdalalg_vector_geom_segmentize.h"
 #include "gdalalg_vector_geom_simplify.h"
+#include "gdalalg_vector_geom_simplify_coverage.h"
 #include "gdalalg_vector_geom_buffer.h"
 #include "gdalalg_vector_geom_swap_xy.h"
 
@@ -39,6 +40,8 @@ GDALVectorGeomAlgorithm::GDALVectorGeomAlgorithm(bool standaloneStep)
     RegisterSubAlgorithm<GDALVectorGeomMakeValidAlgorithm>(standaloneStep);
     RegisterSubAlgorithm<GDALVectorGeomSegmentizeAlgorithm>(standaloneStep);
     RegisterSubAlgorithm<GDALVectorGeomSimplifyAlgorithm>(standaloneStep);
+    RegisterSubAlgorithm<GDALVectorGeomSimplifyCoverageAlgorithm>(
+        standaloneStep);
     RegisterSubAlgorithm<GDALVectorGeomBufferAlgorithm>(standaloneStep);
     RegisterSubAlgorithm<GDALVectorGeomSwapXYAlgorithm>(standaloneStep);
 }
diff --git a/apps/gdalalg_vector_geom_simplify_coverage.cpp b/apps/gdalalg_vector_geom_simplify_coverage.cpp
new file mode 100644
index 000000000000..c6abac15dc28
--- /dev/null
+++ b/apps/gdalalg_vector_geom_simplify_coverage.cpp
@@ -0,0 +1,223 @@
+/******************************************************************************
+*
+ * Project:  GDAL
+ * Purpose:  "gdal vector geom simplify-coverage" subcommand
+ * Author:   Daniel Baston
+ *
+ ******************************************************************************
+ * Copyright (c) 2025, ISciences LLC
+ *
+ * SPDX-License-Identifier: MIT
+ ****************************************************************************/
+
+#include "gdalalg_vector_geom_simplify_coverage.h"
+
+#include "cpl_error.h"
+#include "gdal_priv.h"
+#include "gdalalg_vector_geom.h"
+#include "ogr_geometry.h"
+#include "ogr_geos.h"
+#include "ogrsf_frmts.h"
+
+#ifndef _
+#define _(x) (x)
+#endif
+
+//! @cond Doxygen_Suppress
+
+GDALVectorGeomSimplifyCoverageAlgorithm::
+    GDALVectorGeomSimplifyCoverageAlgorithm(bool standaloneStep)
+    : GDALVectorPipelineStepAlgorithm(NAME, DESCRIPTION, HELP_URL,
+                                      standaloneStep)
+{
+    AddActiveLayerArg(&m_activeLayer);
+    AddArg("tolerance", 0, _("Distance tolerance for simplification."),
+           &m_opts.tolerance)
+        .SetRequired()
+        .SetMinValueIncluded(0);
+    AddArg("preserve-boundary", 0,
+           _("Whether the exterior boundary should be preserved."),
+           &m_opts.preserveBoundary);
+}
+
+#if defined HAVE_GEOS &&                                                       \
+    (GEOS_VERSION_MAJOR > 3 ||                                                 \
+     (GEOS_VERSION_MAJOR == 3 && GEOS_VERSION_MINOR >= 12))
+
+class GDALVectorGeomSimplifyCoverageOutputDataset
+    : public GDALVectorNonStreamingAlgorithmDataset
+{
+  public:
+    GDALVectorGeomSimplifyCoverageOutputDataset(
+        const GDALVectorGeomSimplifyCoverageAlgorithm::Options &opts)
+        : m_opts(opts)
+    {
+    }
+
+    ~GDALVectorGeomSimplifyCoverageOutputDataset()
+    {
+        if (m_poGeosContext != nullptr)
+        {
+            for (size_t i = 0; i < m_nGeosResultSize; i++)
+            {
+                GEOSGeom_destroy_r(m_poGeosContext, m_papoGeosResults[i]);
+            }
+            GEOSFree_r(m_poGeosContext, m_papoGeosResults);
+            finishGEOS_r(m_poGeosContext);
+        }
+    }
+
+    bool Process(OGRLayer &srcLayer, OGRLayer &dstLayer) override
+    {
+        std::vector<OGRFeatureUniquePtr> features;
+        std::vector<GEOSGeometry *> geoms;
+        m_poGeosContext = OGRGeometry::createGEOSContext();
+
+        // Copy features from srcLayer into dstLayer, converting
+        // their geometries to GEOS
+        for (auto &feature : srcLayer)
+        {
+            const OGRGeometry *fgeom = feature->GetGeometryRef();
+
+            // Avoid segfault with non-polygonal inputs on GEOS < 3.12.2
+            // Later versions produce an error instead
+            if (fgeom == nullptr || fgeom->getDimension() != 2)
+            {
+                for (auto &geom : geoms)
+                {
+                    GEOSGeom_destroy_r(m_poGeosContext, geom);
+                }
+                CPLError(CE_Failure, CPLE_AppDefined,
+                         "Coverage simplification can only be performed on "
+                         "polygonal geometries.");
+                return false;
+            }
+
+            geoms.push_back(fgeom->exportToGEOS(m_poGeosContext, false));
+
+            feature->SetGeometry(nullptr);  // free some memory
+            feature->SetFDefnUnsafe(dstLayer.GetLayerDefn());
+
+            features.push_back(std::move(feature));
+        }
+
+        // Perform coverage simplifciation
+        GEOSGeometry *coll = GEOSGeom_createCollection_r(
+            m_poGeosContext, GEOS_GEOMETRYCOLLECTION, geoms.data(),
+            static_cast<unsigned int>(geoms.size()));
+
+        if (coll == nullptr)
+        {
+            for (auto &geom : geoms)
+            {
+                GEOSGeom_destroy_r(m_poGeosContext, geom);
+            }
+            return false;
+        }
+
+        GEOSGeometry *geos_result = GEOSCoverageSimplifyVW_r(
+            m_poGeosContext, coll, m_opts.tolerance, m_opts.preserveBoundary);
+        GEOSGeom_destroy_r(m_poGeosContext, coll);
+
+        if (geos_result == nullptr)
+        {
+            return false;
+        }
+
+        m_papoGeosResults = GEOSGeom_releaseCollection_r(
+            m_poGeosContext, geos_result, &m_nGeosResultSize);
+        GEOSGeom_destroy_r(m_poGeosContext, geos_result);
+        CPLAssert(features.size() == m_nGeosResultSize);
+
+        // Create features with the modified geometries
+        for (size_t i = 0; i < features.size(); i++)
+        {
+            GEOSGeometry *dstGeom = m_papoGeosResults[i];
+
+            std::unique_ptr<OGRGeometry> poSimplified(
+                OGRGeometryFactory::createFromGEOS(m_poGeosContext, dstGeom));
+            GEOSGeom_destroy_r(m_poGeosContext, m_papoGeosResults[i]);
+            m_papoGeosResults[i] = nullptr;
+
+            if (poSimplified == nullptr)
+            {
+                CPLError(CE_Failure, CPLE_AppDefined,
+                         "Failed to convert result from GEOS");
+                return false;
+            }
+            poSimplified->assignSpatialReference(
+                dstLayer.GetLayerDefn()->GetGeomFieldDefn(0)->GetSpatialRef());
+            features[i]->SetGeometry(std::move(poSimplified));
+
+            if (dstLayer.CreateFeature(features[i].get()) != CE_None)
+            {
+                return false;
+            }
+            features[i].reset();
+        }
+
+        return true;
+    }
+
+  private:
+    CPL_DISALLOW_COPY_ASSIGN(GDALVectorGeomSimplifyCoverageOutputDataset)
+
+    const GDALVectorGeomSimplifyCoverageAlgorithm::Options &m_opts;
+    GEOSContextHandle_t m_poGeosContext{nullptr};
+    GEOSGeometry **m_papoGeosResults{nullptr};
+    unsigned int m_nGeosResultSize{0};
+};
+
+bool GDALVectorGeomSimplifyCoverageAlgorithm::RunStep(
+    GDALVectorPipelineStepRunContext &)
+{
+    auto poSrcDS = m_inputDataset[0].GetDatasetRef();
+    auto poDstDS =
+        std::make_unique<GDALVectorGeomSimplifyCoverageOutputDataset>(m_opts);
+
+    bool bFoundActiveLayer = false;
+
+    for (auto &&poSrcLayer : poSrcDS->GetLayers())
+    {
+        if (m_activeLayer.empty() ||
+            m_activeLayer == poSrcLayer->GetDescription())
+        {
+            if (!poDstDS->AddProcessedLayer(*poSrcLayer))
+            {
+                return false;
+            }
+            bFoundActiveLayer = true;
+        }
+        else
+        {
+            poDstDS->AddPassThroughLayer(*poSrcLayer);
+        }
+    }
+
+    if (!bFoundActiveLayer)
+    {
+        ReportError(CE_Failure, CPLE_AppDefined,
+                    "Specified layer '%s' was not found",
+                    m_activeLayer.c_str());
+        return false;
+    }
+
+    m_outputDataset.Set(std::move(poDstDS));
+
+    return true;
+}
+
+#else
+
+bool GDALVectorGeomSimplifyCoverageAlgorithm::RunStep(
+    GDALVectorPipelineStepRunContext &)
+{
+    ReportError(CE_Failure, CPLE_AppDefined,
+                "%s requires GDAL to be built against version 3.12 or later of "
+                "the GEOS library.",
+                NAME);
+    return false;
+}
+#endif  // HAVE_GEOS
+
+//! @endcond
diff --git a/apps/gdalalg_vector_geom_simplify_coverage.h b/apps/gdalalg_vector_geom_simplify_coverage.h
new file mode 100644
index 000000000000..6f744856188c
--- /dev/null
+++ b/apps/gdalalg_vector_geom_simplify_coverage.h
@@ -0,0 +1,70 @@
+/******************************************************************************
+*
+ * Project:  GDAL
+ * Purpose:  "gdal vector geom simplify-coverage"
+ * Author:   Daniel Baston
+ *
+ ******************************************************************************
+ * Copyright (c) 2025, ISciences LLC
+ *
+ * SPDX-License-Identifier: MIT
+ ****************************************************************************/
+
+#ifndef GDALALG_VECTOR_GEOM_SIMPLIFY_COVERAGE_INCLUDED
+#define GDALALG_VECTOR_GEOM_SIMPLIFY_COVERAGE_INCLUDED
+
+#include "gdalalg_vector_pipeline.h"
+#include "cpl_progress.h"
+
+#include <string>
+
+//! @cond Doxygen_Suppress
+
+/************************************************************************/
+/*                GDALVectorGeomSimplifyCoverageAlgorithm               */
+/************************************************************************/
+
+class GDALVectorGeomSimplifyCoverageAlgorithm
+    : public GDALVectorPipelineStepAlgorithm
+{
+  public:
+    static constexpr const char *NAME = "simplify-coverage";
+    static constexpr const char *DESCRIPTION =
+        "Simplify shared boundaries of a polygonal vector dataset.";
+    static constexpr const char *HELP_URL =
+        "/programs/gdal_vector_geom_simplify_coverage.html";
+
+    explicit GDALVectorGeomSimplifyCoverageAlgorithm(
+        bool standaloneStep = false);
+
+    struct Options
+    {
+        double tolerance = 0;
+        bool preserveBoundary = false;
+    };
+
+  private:
+    bool RunStep(GDALVectorPipelineStepRunContext &ctxt) override;
+
+    std::string m_activeLayer{};
+
+    Options m_opts{};
+};
+
+/************************************************************************/
+/*                 GDALVectorGeomSimplifyCoverageStandalone                  */
+/************************************************************************/
+
+class GDALVectorGeomSimplifyCoverageStandalone final
+    : public GDALVectorGeomSimplifyCoverageAlgorithm
+{
+  public:
+    GDALVectorGeomSimplifyCoverageStandalone()
+        : GDALVectorGeomSimplifyCoverageAlgorithm(/* standaloneStep = */ true)
+    {
+    }
+};
+
+//! @endcond
+
+#endif /* GDALALG_VECTOR_GEOM_SIMPLIFY_COVERAGE_INCLUDED */
diff --git a/apps/gdalalg_vector_pipeline.h b/apps/gdalalg_vector_pipeline.h
index ba488cb91225..1505f5211e85 100644
--- a/apps/gdalalg_vector_pipeline.h
+++ b/apps/gdalalg_vector_pipeline.h
@@ -19,6 +19,8 @@
 #include "ogrsf_frmts.h"
 #include "ogrlayerwithtranslatefeature.h"
 
+#include "../frmts/mem/memdataset.h"
+
 #include <map>
 #include <vector>
 
@@ -190,7 +192,7 @@ class GDALVectorPipelineOutputLayer /* non final */
 /************************************************************************/
 
 /** Class that forwards GetNextFeature() calls to the source layer and
- * can be aded to GDALVectorPipelineOutputDataset::AddLayer()
+ * can be added to GDALVectorPipelineOutputDataset::AddLayer()
  */
 class GDALVectorPipelinePassthroughLayer /* non final */
     : public GDALVectorPipelineOutputLayer
@@ -219,6 +221,82 @@ class GDALVectorPipelinePassthroughLayer /* non final */
     }
 };
 
+/**
+ * Dataset used to read all input features into memory and perform some
+ * processing.
+ */
+class GDALVectorNonStreamingAlgorithmDataset : public GDALDataset
+{
+  public:
+    GDALVectorNonStreamingAlgorithmDataset()
+    {
+        GDALDriver *poMemDriver =
+            GetGDALDriverManager()->GetDriverByName("MEM");
+        m_ds.reset(cpl::down_cast<MEMDataset *>(
+            poMemDriver->Create("", 0, 0, 0, GDT_Unknown, nullptr)));
+    }
+
+    virtual bool Process(OGRLayer &srcLayer, OGRLayer &dstLayer) = 0;
+
+    bool AddProcessedLayer(OGRLayer &srcLayer)
+    {
+        CPLStringList aosOptions;
+        if (srcLayer.TestCapability(OLCStringsAsUTF8))
+        {
+            aosOptions.AddNameValue("ADVERTIZE_UTF8", "TRUE");
+        }
+
+        OGRMemLayer *poDstLayer =
+            m_ds->CreateLayer(*srcLayer.GetLayerDefn(), aosOptions.List());
+        m_layers.push_back(poDstLayer);
+
+        if (!Process(srcLayer, *poDstLayer))
+        {
+            return false;
+        }
+
+        poDstLayer->SetUpdatable(false);
+
+        return true;
+    }
+
+    void AddPassThroughLayer(OGRLayer &oLayer)
+    {
+        m_passthrough_layers.push_back(
+            std::make_unique<GDALVectorPipelinePassthroughLayer>(oLayer));
+        m_layers.push_back(m_passthrough_layers.back().get());
+    }
+
+    int GetLayerCount() override
+    {
+        return static_cast<int>(m_layers.size());
+    }
+
+    OGRLayer *GetLayer(int idx) override
+    {
+        if (idx < 0 || idx >= static_cast<int>(m_layers.size()))
+        {
+            return nullptr;
+        }
+        return m_layers[idx];
+    }
+
+    int TestCapability(const char *pszCap) override
+    {
+        if (EQUAL(pszCap, ODsCCreateLayer) || EQUAL(pszCap, ODsCDeleteLayer))
+        {
+            return false;
+        }
+
+        return m_ds->TestCapability(pszCap);
+    }
+
+  private:
+    std::vector<std::unique_ptr<OGRLayer>> m_passthrough_layers{};
+    std::vector<OGRLayer *> m_layers{};
+    std::unique_ptr<MEMDataset> m_ds{};
+};
+
 /************************************************************************/
 /*                 GDALVectorPipelineOutputDataset                      */
 /************************************************************************/
diff --git a/autotest/utilities/test_gdalalg_vector_geom_simplify_coverage.py b/autotest/utilities/test_gdalalg_vector_geom_simplify_coverage.py
new file mode 100644
index 000000000000..d6709ee672d0
--- /dev/null
+++ b/autotest/utilities/test_gdalalg_vector_geom_simplify_coverage.py
@@ -0,0 +1,157 @@
+#!/usr/bin/env pytest
+# -*- coding: utf-8 -*-
+###############################################################################
+# Project:  GDAL/OGR Test Suite
+# Purpose:  'gdal vector geom simplify-coverage' testing
+# Author:   Daniel Baston
+#
+###############################################################################
+# Copyright (c) 2025, ISciences LLC
+#
+# SPDX-License-Identifier: MIT
+###############################################################################
+
+import gdaltest
+import pytest
+
+from osgeo import gdal, ogr
+
+pytestmark = pytest.mark.require_geos(3, 12)
+
+
+@pytest.fixture()
+def alg():
+    return gdal.GetGlobalAlgorithmRegistry()["vector"]["geom"]["simplify-coverage"]
+
+
+def count_points(g):
+    if type(g) is ogr.Layer:
+        return sum(count_points(f) for f in g)
+
+    if type(g) is ogr.Feature:
+        return count_points(g.GetGeometryRef())
+
+    if g.GetGeometryCount():
+        return sum(count_points(subg) for subg in g)
+    else:
+        return g.GetPointCount()
+
+
+def test_gdalalg_vector_geom_simplify_coverage(alg):
+
+    src_ds = gdal.OpenEx("../ogr/data/poly.shp", gdal.OF_VECTOR)
+    src_lyr = src_ds.GetLayer(0)
+
+    alg["input"] = src_ds
+    alg["output"] = ""
+    alg["output-format"] = "stream"
+    alg["tolerance"] = 2
+
+    assert alg.Run()
+
+    dst_ds = alg["output"].GetDataset()
+    dst_lyr = dst_ds.GetLayer(0)
+
+    assert dst_lyr.GetFeatureCount() == src_lyr.GetFeatureCount()
+    assert dst_lyr.GetSpatialRef().IsSame(src_lyr.GetSpatialRef())
+
+    assert count_points(dst_lyr) < count_points(src_lyr)
+
+    assert alg.Finalize()
+
+
+def test_gdalalg_vector_geom_simplify_coverage_active_layer(alg):
+
+    src_ds = gdal.GetDriverByName("MEM").CreateVector("")
+
+    with gdal.OpenEx("../ogr/data/poly.shp", gdal.OF_VECTOR) as poly:
+        src_ds.CopyLayer(poly.GetLayer(0), "poly1")
+        src_ds.CopyLayer(poly.GetLayer(0), "poly2")
+
+    alg["input"] = src_ds
+    alg["output"] = ""
+    alg["output-format"] = "stream"
+    alg["tolerance"] = 2
+    alg["active-layer"] = "bad"
+
+    with pytest.raises(RuntimeError, match="layer .* was not found"):
+        alg.Run()
+
+    alg["active-layer"] = "poly2"
+    assert alg.Run()
+
+    dst_ds = alg["output"].GetDataset()
+
+    assert dst_ds.GetLayerCount() == 2
+
+    assert (
+        dst_ds.GetLayerByName("poly1").GetFeatureCount()
+        == dst_ds.GetLayerByName("poly2").GetFeatureCount()
+    )
+
+    assert count_points(dst_ds.GetLayerByName("poly2")) < count_points(
+        dst_ds.GetLayerByName("poly1")
+    )
+
+    assert alg.Finalize()
+
+
+@pytest.mark.parametrize("geom", (ogr.CreateGeometryFromWkt("POINT (3 8)"), None))
+def test_gdalalg_vector_geom_simplify_coverage_non_polygonal_inputs(
+    alg, geom, tmp_vsimem
+):
+
+    src_ds = gdal.GetDriverByName("MEM").CreateVector("")
+    src_lyr = src_ds.CreateLayer("layer")
+    src_lyr.CreateField(ogr.FieldDefn("a"))
+
+    f = ogr.Feature(src_lyr.GetLayerDefn())
+    f["a"] = 1
+    f.SetGeometryDirectly(ogr.CreateGeometryFromWkt("POINT (3 8)"))
+    src_lyr.CreateFeature(f)
+
+    alg["input"] = src_ds
+    alg["output"] = tmp_vsimem / "out.shp"
+    alg["tolerance"] = 2
+
+    with pytest.raises(RuntimeError, match="can only be performed on polygonal"):
+        assert alg.Run()
+
+
+def test_gdalalg_vector_geom_simplify_coverage_no_tolerance(alg):
+
+    alg["input"] = "../ogr/data/poly.shp"
+    alg["output"] = ""
+    alg["output-format"] = "stream"
+
+    with pytest.raises(RuntimeError, match="tolerance. has not been specified"):
+        assert alg.Run()
+
+
+@pytest.mark.parametrize("tol", (-5, float("nan")))
+def test_gdalalg_vector_geom_simplify_coverage_bad_tolerance(alg, tol):
+
+    with pytest.raises(RuntimeError, match="should be >= 0"):
+        alg["tolerance"] = tol
+
+
+@pytest.mark.require_driver("GDALG")
+def test_gdalalg_vector_geom_simplify_coverage_test_ogrsf(tmp_path):
+
+    import test_cli_utilities
+
+    if test_cli_utilities.get_test_ogrsf_path() is None:
+        pytest.skip()
+
+    gdalg_filename = tmp_path / "tmp.gdalg.json"
+    open(gdalg_filename, "wb").write(
+        b'{"type": "gdal_streamed_alg","command_line": "gdal vector geom simplify-coverage --tolerance 10 ../ogr/data/poly.shp --output-format=stream dummy_dataset_name","relative_paths_relative_to_this_file":false}'
+    )
+
+    ret = gdaltest.runexternal(
+        test_cli_utilities.get_test_ogrsf_path() + f" -ro {gdalg_filename}"
+    )
+
+    assert "INFO" in ret
+    assert "ERROR" not in ret
+    assert "FAILURE" not in ret
diff --git a/doc/images/programs/gdal_vector_geom_simplify_coverage.svg b/doc/images/programs/gdal_vector_geom_simplify_coverage.svg
new file mode 100644
index 000000000000..a8dd1abcbdee
--- /dev/null
+++ b/doc/images/programs/gdal_vector_geom_simplify_coverage.svg
@@ -0,0 +1,419 @@
+<?xml version="1.0" encoding="utf-8" standalone="no"?>
+<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN"
+  "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
+<svg xmlns:xlink="http://www.w3.org/1999/xlink" width="684pt" height="264.641942pt" viewBox="0 0 684 264.641942" xmlns="http://www.w3.org/2000/svg" version="1.1">
+ <metadata>
+  <rdf:RDF xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:cc="http://creativecommons.org/ns#" xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#">
+   <cc:Work>
+    <dc:type rdf:resource="http://purl.org/dc/dcmitype/StillImage"/>
+    <dc:date>2025-05-18T10:19:44.280578</dc:date>
+    <dc:format>image/svg+xml</dc:format>
+    <dc:creator>
+     <cc:Agent>
+      <dc:title>Matplotlib v3.10.3, https://matplotlib.org/</dc:title>
+     </cc:Agent>
+    </dc:creator>
+   </cc:Work>
+  </rdf:RDF>
+ </metadata>
+ <defs>
+  <style type="text/css">*{stroke-linejoin: round; stroke-linecap: butt}</style>
+ </defs>
+ <g id="figure_1">
+  <g id="patch_1">
+   <path d="M 0 264.641942 
+L 684 264.641942 
+L 684 0 
+L 0 0 
+L 0 264.641942 
+z
+" style="fill: none"/>
+  </g>
+  <g id="axes_1">
+   <g id="LineCollection_1">
+    <path d="M 146.038288 54.625449 
+L 135.264266 48.060801 
+L 131.675522 38.920151 
+L 138.603719 36.34415 
+L 164.252071 24.793693 
+L 163.945651 23.214853 
+L 174.286011 18.893819 
+L 177.817626 29.57176 
+L 191.339035 68.336424 
+L 193.390488 73.779265 
+L 172.141074 81.548817 
+L 167.679814 71.327909 
+L 167.903137 65.511132 
+L 168.388734 58.032419 
+L 165.986717 49.722737 
+L 162.647263 43.074992 
+L 159.364939 35.596279 
+L 153.519598 38.878603 
+L 149.364757 47.188284 
+L 146.038288 54.625449 
+" clip-path="url(#p58459cba9d)" style="fill: none; stroke: #1f77b4; stroke-width: 1.5"/>
+    <path d="M 163.945651 23.214853 
+L 164.252071 24.793693 
+L 138.603719 36.34415 
+L 131.675522 38.920151 
+L 135.264266 48.060801 
+L 146.038288 54.625449 
+L 142.714415 60.525323 
+L 134.565734 74.610233 
+L 116.801193 74.402491 
+L 112.101029 74.568685 
+L 107.245059 74.651782 
+L 95.250553 79.3883 
+L 94.266375 79.679139 
+L 88.156163 68.377972 
+L 81.778482 59.902097 
+L 80.381417 60.442226 
+L 79.153142 57.367644 
+L 90.371212 52.963513 
+L 131.062683 36.427247 
+L 163.945651 23.214853 
+" clip-path="url(#p58459cba9d)" style="fill: none; stroke: #1f77b4; stroke-width: 1.5"/>
+    <path d="M 146.038288 54.625449 
+L 149.364757 47.188284 
+L 153.519598 38.878603 
+L 159.364939 35.596279 
+L 162.647263 43.074992 
+L 165.986717 49.722737 
+L 168.388734 58.032419 
+L 167.903137 65.511132 
+L 161.032069 66.051261 
+L 155.604808 67.962488 
+L 152.29392 69.624424 
+L 138.936107 80.842494 
+L 134.441088 81.922753 
+L 131.491151 83.958625 
+L 130.917264 85.994497 
+L 129.200795 89.56766 
+L 126.713085 93.057726 
+L 123.451535 95.093598 
+L 119.867984 102.07373 
+L 97.839538 110.757347 
+L 87.701727 82.338237 
+L 94.266375 79.679139 
+L 95.250553 79.3883 
+L 107.245059 74.651782 
+L 112.101029 74.568685 
+L 116.801193 74.402491 
+L 134.565734 74.610233 
+L 142.714415 60.525323 
+L 146.038288 54.625449 
+" clip-path="url(#p58459cba9d)" style="fill: none; stroke: #1f77b4; stroke-width: 1.5"/>
+    <path d="M 79.153142 57.367644 
+L 80.381417 60.442226 
+L 87.701727 82.338237 
+L 97.839538 110.757347 
+L 103.329122 122.723289 
+L 99.187265 126.587291 
+L 96.268489 135.063166 
+L 87.426469 138.262393 
+L 79.430997 117.945222 
+L 69.347718 121.892321 
+L 57.039002 127.085872 
+L 36.615363 134.606133 
+L 33.185023 124.676064 
+L 31.704861 119.440965 
+L 32.026861 114.995285 
+L 32.099571 108.555282 
+L 36.545251 107.724314 
+L 51.570193 92.019016 
+L 46.420788 87.199401 
+L 44.452432 89.02753 
+L 39.064123 94.927404 
+L 33.049991 101.616698 
+L 28.723763 98.375922 
+L 21.034711 79.928429 
+L 68.708911 61.314743 
+L 79.153142 57.367644 
+" clip-path="url(#p58459cba9d)" style="fill: none; stroke: #1f77b4; stroke-width: 1.5"/>
+    <path d="M 80.381417 60.442226 
+L 81.778482 59.902097 
+L 88.156163 68.377972 
+L 94.266375 79.679139 
+L 87.701727 82.338237 
+L 80.381417 60.442226 
+" clip-path="url(#p58459cba9d)" style="fill: none; stroke: #1f77b4; stroke-width: 1.5"/>
+    <path d="M 167.903137 65.511132 
+L 167.679814 71.327909 
+L 172.141074 81.548817 
+L 158.388552 87.240949 
+L 140.291623 94.387275 
+L 139.063349 90.232434 
+L 131.101635 92.808436 
+L 132.638926 97.046373 
+L 119.867984 102.07373 
+L 123.451535 95.093598 
+L 126.713085 93.057726 
+L 129.200795 89.56766 
+L 130.917264 85.994497 
+L 131.491151 83.958625 
+L 134.441088 81.922753 
+L 138.936107 80.842494 
+L 152.29392 69.624424 
+L 155.604808 67.962488 
+L 161.032069 66.051261 
+L 167.903137 65.511132 
+" clip-path="url(#p58459cba9d)" style="fill: none; stroke: #1f77b4; stroke-width: 1.5"/>
+    <path d="M 193.390488 73.779265 
+L 205.644671 68.461069 
+L 208.205092 76.438363 
+L 211.282271 83.335399 
+L 219.264759 91.39579 
+L 220.028211 95.342888 
+L 209.918964 91.188048 
+L 205.940704 91.146499 
+L 203.813945 94.968953 
+L 221.807002 106.062377 
+L 218.527274 115.036833 
+L 217.335354 116.241737 
+L 215.465676 119.690255 
+L 209.568399 123.388063 
+L 202.590863 125.590129 
+L 190.801503 129.703421 
+L 176.989254 137.514522 
+L 174.065285 130.575938 
+L 180.47932 127.459807 
+L 185.034064 123.761999 
+L 187.267291 119.897997 
+L 189.557647 113.084058 
+L 189.570631 108.472185 
+L 182.450273 88.985982 
+L 175.729818 91.063402 
+L 168.799024 94.304178 
+L 161.922763 96.672437 
+L 159.767439 91.894371 
+L 158.388552 87.240949 
+L 172.141074 81.548817 
+L 193.390488 73.779265 
+" clip-path="url(#p58459cba9d)" style="fill: none; stroke: #1f77b4; stroke-width: 1.5"/>
+    <path d="M 219.264759 91.39579 
+L 224.284325 88.071917 
+L 229.560973 84.581851 
+L 237.626558 82.047398 
+L 238.348461 81.922753 
+L 242.796738 80.011526 
+L 251.433613 74.94262 
+L 255.476792 70.081457 
+L 267.481685 79.886881 
+L 268.294476 85.412819 
+L 282.820838 80.219268 
+L 282.111918 74.859524 
+L 286.456323 72.366619 
+L 288.886905 71.078619 
+L 291.886181 69.665973 
+L 294.140182 76.646105 
+L 296.443522 83.792431 
+L 297.728926 86.451529 
+L 296.95249 86.617723 
+L 263.659232 98.708309 
+L 261.540263 100.079407 
+L 260.659956 100.910375 
+L 259.519972 102.281472 
+L 257.395809 104.525086 
+L 255.778018 110.300315 
+L 255.874099 112.793219 
+L 260.711892 137.763812 
+L 256.024712 151.848722 
+L 254.934066 154.424724 
+L 247.424192 161.903437 
+L 234.357217 180.143188 
+L 229.65186 181.763575 
+L 222.931405 183.591705 
+L 215.39037 183.674802 
+L 210.851206 180.974156 
+L 184.436806 187.91274 
+L 179.414642 191.485903 
+L 177.602093 193.521775 
+L 177.220367 200.086423 
+L 183.751257 210.431976 
+L 186.742743 230.541406 
+L 177.781271 238.934184 
+L 148.985628 245.748123 
+L 148.422127 244.377025 
+L 138.479074 217.744496 
+L 118.852645 165.185761 
+L 123.620325 159.410532 
+L 170.998493 137.057489 
+L 176.989254 137.514522 
+L 190.801503 129.703421 
+L 202.590863 125.590129 
+L 209.568399 123.388063 
+L 215.465676 119.690255 
+L 217.335354 116.241737 
+L 218.527274 115.036833 
+L 221.807002 106.062377 
+L 203.813945 94.968953 
+L 205.940704 91.146499 
+L 209.918964 91.188048 
+L 220.028211 95.342888 
+L 219.264759 91.39579 
+" clip-path="url(#p58459cba9d)" style="fill: none; stroke: #1f77b4; stroke-width: 1.5"/>
+    <path d="M 140.291623 94.387275 
+L 158.388552 87.240949 
+L 159.767439 91.894371 
+L 161.922763 96.672437 
+L 168.799024 94.304178 
+L 175.729818 91.063402 
+L 182.450273 88.985982 
+L 189.570631 108.472185 
+L 189.557647 113.084058 
+L 187.267291 119.897997 
+L 185.034064 123.761999 
+L 180.47932 127.459807 
+L 174.065285 130.575938 
+L 176.989254 137.514522 
+L 170.998493 137.057489 
+L 123.620325 159.410532 
+L 118.852645 165.185761 
+L 118.442355 163.980857 
+L 114.702998 153.552207 
+L 114.142095 150.810012 
+L 111.062319 144.411557 
+L 106.964607 133.608971 
+L 103.329122 122.723289 
+L 97.839538 110.757347 
+L 119.867984 102.07373 
+L 132.638926 97.046373 
+L 140.291623 94.387275 
+" clip-path="url(#p58459cba9d)" style="fill: none; stroke: #1f77b4; stroke-width: 1.5"/>
+    <path d="M 140.291623 94.387275 
+L 132.638926 97.046373 
+L 131.101635 92.808436 
+L 139.063349 90.232434 
+L 140.291623 94.387275 
+" clip-path="url(#p58459cba9d)" style="fill: none; stroke: #1f77b4; stroke-width: 1.5"/>
+   </g>
+  </g>
+  <g id="axes_2">
+   <g id="LineCollection_2">
+    <path d="M 511.626413 54.406813 
+L 497.223231 38.65732 
+L 529.584168 22.907828 
+L 539.953625 18.574634 
+L 559.111863 73.614528 
+L 537.802653 81.405944 
+L 533.55279 65.323128 
+L 524.990566 35.324094 
+L 511.626413 54.406813 
+" clip-path="url(#p07d3865e51)" style="fill: none; stroke: #1f77b4; stroke-width: 1.5"/>
+    <path d="M 529.584168 22.907828 
+L 497.223231 38.65732 
+L 511.626413 54.406813 
+L 500.121575 74.447834 
+L 459.708814 79.531004 
+L 445.784783 60.239959 
+L 444.553052 57.156725 
+L 529.584168 22.907828 
+" clip-path="url(#p07d3865e51)" style="fill: none; stroke: #1f77b4; stroke-width: 1.5"/>
+    <path d="M 511.626413 54.406813 
+L 524.990566 35.324094 
+L 533.55279 65.323128 
+L 497.038341 83.822533 
+L 485.382466 101.988614 
+L 463.292032 110.696667 
+L 453.125692 82.197585 
+L 459.708814 79.531004 
+L 500.121575 74.447834 
+L 511.626413 54.406813 
+" clip-path="url(#p07d3865e51)" style="fill: none; stroke: #1f77b4; stroke-width: 1.5"/>
+    <path d="M 444.553052 57.156725 
+L 445.784783 60.239959 
+L 453.125692 82.197585 
+L 463.292032 110.696667 
+L 468.797063 122.696281 
+L 452.84966 138.279112 
+L 444.831689 117.904768 
+L 401.895571 134.612564 
+L 396.971251 119.40472 
+L 416.892484 91.905606 
+L 398.320166 101.530296 
+L 386.271074 79.780996 
+L 444.553052 57.156725 
+" clip-path="url(#p07d3865e51)" style="fill: none; stroke: #1f77b4; stroke-width: 1.5"/>
+    <path d="M 445.784783 60.239959 
+L 459.708814 79.531004 
+L 453.125692 82.197585 
+L 445.784783 60.239959 
+" clip-path="url(#p07d3865e51)" style="fill: none; stroke: #1f77b4; stroke-width: 1.5"/>
+    <path d="M 533.55279 65.323128 
+L 537.802653 81.405944 
+L 524.011431 87.114093 
+L 505.863577 94.280529 
+L 496.647728 92.697247 
+L 498.189345 96.94711 
+L 485.382466 101.988614 
+L 497.038341 83.822533 
+L 533.55279 65.323128 
+" clip-path="url(#p07d3865e51)" style="fill: none; stroke: #1f77b4; stroke-width: 1.5"/>
+    <path d="M 559.111863 73.614528 
+L 571.400529 68.281366 
+L 585.058944 91.280626 
+L 569.564651 94.863844 
+L 587.608341 105.988485 
+L 581.249171 119.654712 
+L 542.664476 137.529136 
+L 555.281257 108.405074 
+L 548.140862 88.864037 
+L 527.555587 96.572122 
+L 524.011431 87.114093 
+L 537.802653 81.405944 
+L 559.111863 73.614528 
+" clip-path="url(#p07d3865e51)" style="fill: none; stroke: #1f77b4; stroke-width: 1.5"/>
+    <path d="M 585.058944 91.280626 
+L 621.372879 69.906314 
+L 634.226631 85.280819 
+L 657.884724 69.489661 
+L 662.965289 86.489113 
+L 629.578344 98.613723 
+L 621.771303 112.738268 
+L 626.62271 137.779128 
+L 620.828625 154.486924 
+L 600.193873 180.27776 
+L 550.132985 188.069176 
+L 542.896239 200.277116 
+L 552.445411 230.817799 
+L 543.458721 239.234195 
+L 514.582047 246.067308 
+L 484.36427 165.278243 
+L 489.145366 159.486763 
+L 542.664476 137.529136 
+L 581.249171 119.654712 
+L 587.608341 105.988485 
+L 569.564651 94.863844 
+L 585.058944 91.280626 
+" clip-path="url(#p07d3865e51)" style="fill: none; stroke: #1f77b4; stroke-width: 1.5"/>
+    <path d="M 505.863577 94.280529 
+L 524.011431 87.114093 
+L 527.555587 96.572122 
+L 548.140862 88.864037 
+L 555.281257 108.405074 
+L 542.664476 137.529136 
+L 489.145366 159.486763 
+L 484.36427 165.278243 
+L 468.797063 122.696281 
+L 463.292032 110.696667 
+L 485.382466 101.988614 
+L 498.189345 96.94711 
+L 505.863577 94.280529 
+" clip-path="url(#p07d3865e51)" style="fill: none; stroke: #1f77b4; stroke-width: 1.5"/>
+    <path d="M 498.189345 96.94711 
+L 496.647728 92.697247 
+L 505.863577 94.280529 
+L 498.189345 96.94711 
+" clip-path="url(#p07d3865e51)" style="fill: none; stroke: #1f77b4; stroke-width: 1.5"/>
+   </g>
+  </g>
+ </g>
+ <defs>
+  <clipPath id="p58459cba9d">
+   <rect x="7.2" y="7.551104" width="304.363636" height="249.539734"/>
+  </clipPath>
+  <clipPath id="p07d3865e51">
+   <rect x="372.436364" y="7.2" width="304.363636" height="250.241942"/>
+  </clipPath>
+ </defs>
+</svg>
diff --git a/doc/source/programs/gdal_vector_geom.rst b/doc/source/programs/gdal_vector_geom.rst
index e2e48abc01c8..4036c0499b76 100644
--- a/doc/source/programs/gdal_vector_geom.rst
+++ b/doc/source/programs/gdal_vector_geom.rst
@@ -25,5 +25,6 @@ Available sub-commands
 - :ref:`gdal_vector_geom_make_valid`
 - :ref:`gdal_vector_geom_segmentize`
 - :ref:`gdal_vector_geom_simplify`
+- :ref:`gdal_vector_geom_simplify_coverage`
 - :ref:`gdal_vector_geom_buffer`
 - :ref:`gdal_vector_geom_swap_xy`
diff --git a/doc/source/programs/gdal_vector_geom_simplify.rst b/doc/source/programs/gdal_vector_geom_simplify.rst
index cb88f9987890..23d53ff8c351 100644
--- a/doc/source/programs/gdal_vector_geom_simplify.rst
+++ b/doc/source/programs/gdal_vector_geom_simplify.rst
@@ -20,7 +20,7 @@ Synopsis
 Description
 -----------
 
-:program:`gdal vector geom simplify` simplify geometries, ensuring that
+:program:`gdal vector geom simplify` simplifies geometries, ensuring that
 the result is a valid geometry having the same dimension and number of
 components as the input.
 
@@ -30,7 +30,7 @@ similar to the one used in the Douglas-Peucker algorithm.
 This done by the :cpp:func:`OGRGeometry::SimplifyPreserveTopology` method which
 preserves the topology per feature, but not for a whole layer.
 Thus gaps or overlaps between geometries that were initially contiguous may
-happen.
+happen. To perform simplification that preserves shared boundaries between geometries, see :ref:`gdal_vector_geom_simplify_coverage`.
 
 This command can also be used as a step of :ref:`gdal_vector_pipeline`.
 
diff --git a/doc/source/programs/gdal_vector_geom_simplify_coverage.rst b/doc/source/programs/gdal_vector_geom_simplify_coverage.rst
new file mode 100644
index 000000000000..a8807e8ec66b
--- /dev/null
+++ b/doc/source/programs/gdal_vector_geom_simplify_coverage.rst
@@ -0,0 +1,112 @@
+.. _gdal_vector_geom_simplify_coverage:
+
+================================================================================
+``gdal vector geom simplify-coverage``
+================================================================================
+
+.. versionadded:: 3.12
+
+.. only:: html
+
+    Simplify boundaries of a polygonal vector dataset.
+
+.. Index:: gdal vector geom simplify-coverage
+
+
+
+Synopsis
+--------
+
+.. program-output:: gdal vector geom simplify-coverage --help-doc
+
+Description
+-----------
+
+:program:`gdal vector geom simplify-coverage` simplifies boundaries of a
+polygonal dataset, such that shared boundaries are preserved without
+introducing gaps or overlaps between features. Gaps or overlaps already present
+in the input dataset will not be corrected.
+
+This requires loading the entire dataset into memory at once. If preservation
+of shared boundaries is not needed, :ref:`gdal_vector_geom_simplify` provides
+an alternative that can process geometries in a streaming manner.
+
+Simplification is performed using the Visvalingam-Whyatt algorithm.
+
+This command can also be used as a step of :ref:`gdal_vector_pipeline`.
+
+.. only:: html
+
+   .. figure:: ../../images/programs/gdal_vector_geom_simplify_coverage.svg
+
+   Polygon dataset before (left) and after (right) simplification with :program:`gdal vector geom simplify-coverage`.
+
+.. note:: This command requires a GDAL build against the GEOS library (version 3.12 or greater).
+
+Standard options
+++++++++++++++++
+
+.. option:: --tolerance <TOLERANCE>
+
+    Tolerance used for determining whether vertices should be removed.
+    Specified in georeferenced units of the source layer.
+
+.. option:: --preserve-external-boundary
+
+    Flag indicating whether to preserve (avoid simplifying) external boundaries.
+    This can be useful when simplifying a portion of a larger dataset.
+
+.. include:: gdal_options/active_layer.rst
+
+.. include:: gdal_options/of_vector.rst
+
+.. include:: gdal_options/co_vector.rst
+
+.. include:: options/lco.rst
+
+.. include:: gdal_options/overwrite.rst
+
+Advanced options
+++++++++++++++++
+
+.. include:: gdal_options/oo.rst
+
+.. include:: gdal_options/if.rst
+
+.. GDALG output (on-the-fly / streamed dataset)
+.. --------------------------------------------
+
+.. include:: gdal_cli_include/gdalg_vector_compatible.rst
+
+Examples
+--------
+
+.. example::
+   :title: Simplify geometries using a tolerance of one square meter (assuming the CRS is in meters)
+
+   .. code-block:: bash
+
+        $ gdal vector geom simplify-coverage --tolerance=1 in.gpkg out.gpkg --overwrite
+
+
+.. example::
+   :title: Using simplify-coverage as part of a vector pipeline
+
+   This can be used to make the output geometries have a consistent type.
+
+   .. code-block:: bash
+
+       $ gdal vector pipeline ! read tl_2024_us_state.shp !  geom simplify-coverage --tolerance 2 ! geom set-type --multi ! write out.gpkg --overwrite
+
+
+
+
+
+
+
+.. below is an allow-list for spelling checker.
+
+.. spelling:word-list::
+        Visvalingam
+        Whyatt
+
diff --git a/doc/source/programs/index.rst b/doc/source/programs/index.rst
index 67635a582bc7..290a9f8a3243 100644
--- a/doc/source/programs/index.rst
+++ b/doc/source/programs/index.rst
@@ -171,6 +171,7 @@ Vector commands
    gdal_vector_geom_make_valid
    gdal_vector_geom_segmentize
    gdal_vector_geom_simplify
+   gdal_vector_geom_simplify_coverage
    gdal_vector_geom_buffer
    gdal_vector_geom_swap_xy
    gdal_vector_grid
@@ -196,6 +197,7 @@ Vector commands
     - :ref:`gdal_vector_geom_make_valid`: Fix validity of geometries of a vector dataset
     - :ref:`gdal_vector_geom_segmentize`: Segmentize geometries of a vector dataset
     - :ref:`gdal_vector_geom_simplify`: Simplify geometries of a vector dataset
+    - :ref:`gdal_vector_geom_simplify_coverage`: Simplify shared boundaries of a polygonal vector dataset
     - :ref:`gdal_vector_geom_buffer`: Compute a buffer around geometries of a vector dataset
     - :ref:`gdal_vector_geom_swap_xy`: Swap X and Y coordinates of geometries of a vector dataset
     - :ref:`gdal_vector_grid`: Create a regular grid from scattered points
diff --git a/frmts/mem/memdataset.cpp b/frmts/mem/memdataset.cpp
index 3d218d3689da..a67fa916976b 100644
--- a/frmts/mem/memdataset.cpp
+++ b/frmts/mem/memdataset.cpp
@@ -3216,6 +3216,26 @@ static CPLErr MEMDatasetDelete(const char * /* fileName */)
     return CE_None;
 }
 
+/************************************************************************/
+/*                            CreateLayer()                             */
+/************************************************************************/
+
+OGRMemLayer *MEMDataset::CreateLayer(const OGRFeatureDefn &oDefn,
+                                     CSLConstList papszOptions)
+{
+    auto poLayer = std::make_unique<OGRMemLayer>(oDefn);
+
+    if (CPLFetchBool(papszOptions, "ADVERTIZE_UTF8", false))
+        poLayer->SetAdvertizeUTF8(true);
+
+    poLayer->SetDataset(this);
+    poLayer->SetFIDColumn(CSLFetchNameValueDef(papszOptions, "FID", ""));
+
+    // Add layer to data source layer list.
+    m_apoLayers.emplace_back(std::move(poLayer));
+    return m_apoLayers.back().get();
+}
+
 /************************************************************************/
 /*                           ICreateLayer()                             */
 /************************************************************************/
diff --git a/frmts/mem/memdataset.h b/frmts/mem/memdataset.h
index 19519df343bb..cd01d5a6af43 100644
--- a/frmts/mem/memdataset.h
+++ b/frmts/mem/memdataset.h
@@ -135,6 +135,9 @@ class CPL_DLL MEMDataset CPL_NON_FINAL : public GDALDataset
 
     OGRLayer *GetLayer(int) override;
 
+    OGRMemLayer *CreateLayer(const OGRFeatureDefn &oDefn,
+                             CSLConstList papszOptions);
+
     OGRLayer *ICreateLayer(const char *pszName,
                            const OGRGeomFieldDefn *poGeomFieldDefn,
                            CSLConstList papszOptions) override;
@@ -252,6 +255,7 @@ class CPL_DLL OGRMemLayer CPL_NON_FINAL : public OGRLayer
     // Clone poSRS if not nullptr
     OGRMemLayer(const char *pszName, const OGRSpatialReference *poSRS,
                 OGRwkbGeometryType eGeomType);
+    explicit OGRMemLayer(const OGRFeatureDefn &oFeatureDefn);
     virtual ~OGRMemLayer();
 
     void ResetReading() override;
diff --git a/frmts/mem/ogrmemlayer.cpp b/frmts/mem/ogrmemlayer.cpp
index 39a142fbc122..8f6699d315ba 100644
--- a/frmts/mem/ogrmemlayer.cpp
+++ b/frmts/mem/ogrmemlayer.cpp
@@ -71,6 +71,17 @@ OGRMemLayer::OGRMemLayer(const char *pszName,
     m_poFeatureDefn->Seal(/* bSealFields = */ true);
 }
 
+OGRMemLayer::OGRMemLayer(const OGRFeatureDefn &oFeatureDefn)
+    : m_poFeatureDefn(oFeatureDefn.Clone())
+{
+    m_poFeatureDefn->Reference();
+
+    SetDescription(m_poFeatureDefn->GetName());
+
+    m_oMapFeaturesIter = m_oMapFeatures.begin();
+    m_poFeatureDefn->Seal(/* bSealFields = */ true);
+}
+
 /************************************************************************/
 /*                           ~OGRMemLayer()                           */
 /************************************************************************/


Reviews:
user: login: rouault
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-13T15:05:35Z
user: login: rouault
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-13T20:06:51Z
user: login: jratike80
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-05-13T20:23:58Z
user: login: dbaston
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-14T01:27:40Z
user: login: dbaston
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-14T01:27:56Z
user: login: dbaston
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-14T01:28:40Z
user: login: dbaston
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-14T01:30:50Z
user: login: jratike80
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-05-14T06:10:24Z
user: login: rouault
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-14T08:15:10Z
user: login: rouault
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-15T15:27:59Z
user: login: rouault
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-15T15:32:21Z
user: login: dbaston
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-16T17:53:04Z
user: login: rouault
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-16T17:59:51Z
user: login: rouault
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-18T01:31:13Z
user: login: dbaston
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-18T01:39:15Z
user: login: rouault
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-18T12:48:09Z
user: login: dbaston
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-18T13:31:25Z
user: login: dbaston
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-18T13:31:42Z
user: login: rouault
state: APPROVED
author_association: MEMBER
submitted_at: 2025-05-18T13:31:56Z

Review comments:
diff_hunk: @@ -0,0 +1,352 @@
+/******************************************************************************
+*
+ * Project:  GDAL
+ * Purpose:  "gdal vector geom simplify-coverage" subcommand
+ * Author:   Daniel Baston
+ *
+ ******************************************************************************
+ * Copyright (c) 2025, ISciences LLC
+ *
+ * SPDX-License-Identifier: MIT
+ ****************************************************************************/
+
+#include "gdalalg_vector_geom_simplify_coverage.h"
+
+#include "cpl_error.h"
+#include "gdal_priv.h"
+#include "gdalalg_vector_geom.h"
+#include "ogr_geometry.h"
+#include "ogr_geos.h"
+#include "ogrsf_frmts.h"
+
+#ifndef _
+#define _(x) (x)
+#endif
+
+//! @cond Doxygen_Suppress
+
+GDALVectorGeomSimplifyCoverageAlgorithm::
+    GDALVectorGeomSimplifyCoverageAlgorithm(bool standaloneStep)
+    : GDALVectorPipelineStepAlgorithm(NAME, DESCRIPTION, HELP_URL,
+                                      standaloneStep)
+{
+    AddActiveLayerArg(&m_activeLayer);
+    AddArg("tolerance", 0, _("Distance tolerance for simplification."),
+           &m_opts.tolerance)
+        .SetRequired()
+        .SetMinValueIncluded(0);
+    AddArg("preserve-boundary", 0,
+           _("Whether the exterior boundary should be preserved."),
+           &m_opts.preserveBoundary);
+}
+
+#if defined HAVE_GEOS &&                                                       \
+    (GEOS_VERSION_MAJOR > 3 ||                                                 \
+     (GEOS_VERSION_MAJOR == 3 && GEOS_VERSION_MINOR >= 12))
+
+/** Class used by vector pipeline steps to create an output on-the-fly
+ * dataset where they can store on-the-fly layers.
+ */
+class GDALVectorGeomSimplifyCoverageOutputDataset final : public GDALDataset
+{
+  public:
+    GDALVectorGeomSimplifyCoverageOutputDataset(GDALDataset &srcDS);
+
+    void AddLayer(std::unique_ptr<OGRLayer> poNewLayer)
+    {
+        m_layers.push_back(std::move(poNewLayer));
+    }
+
+    int GetLayerCount() override
+    {
+        return static_cast<int>(m_layers.size());
+    }
+
+    OGRLayer *GetLayer(int idx) override
+    {
+        return m_layers[idx].get();
+    }
+
+    int TestCapability(const char *) override
+    {
+        // FIXME implement?
+        return false;
+    }
+
+    void ResetReading() override
+    {
+        m_srcDS.ResetReading();
+    }
+
+    OGRFeature *GetNextFeature(OGRLayer **, double *, GDALProgressFunc,
+                               void *) override
+    {
+        // FIXME implement?
+        CPLError(CE_Failure, CPLE_AppDefined,
+                 "GDALDataset::GetNextFeature not implemented.");
+        return nullptr;
+    }
path: apps/gdalalg_vector_geom_simplify_coverage.cpp
user: login: rouault
body: The base implementation of GDALDataset will do the right thing:

```suggestion
```
created_at: 2025-05-13T15:01:03Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,352 @@
+/******************************************************************************
+*
+ * Project:  GDAL
+ * Purpose:  "gdal vector geom simplify-coverage" subcommand
+ * Author:   Daniel Baston
+ *
+ ******************************************************************************
+ * Copyright (c) 2025, ISciences LLC
+ *
+ * SPDX-License-Identifier: MIT
+ ****************************************************************************/
+
+#include "gdalalg_vector_geom_simplify_coverage.h"
+
+#include "cpl_error.h"
+#include "gdal_priv.h"
+#include "gdalalg_vector_geom.h"
+#include "ogr_geometry.h"
+#include "ogr_geos.h"
+#include "ogrsf_frmts.h"
+
+#ifndef _
+#define _(x) (x)
+#endif
+
+//! @cond Doxygen_Suppress
+
+GDALVectorGeomSimplifyCoverageAlgorithm::
+    GDALVectorGeomSimplifyCoverageAlgorithm(bool standaloneStep)
+    : GDALVectorPipelineStepAlgorithm(NAME, DESCRIPTION, HELP_URL,
+                                      standaloneStep)
+{
+    AddActiveLayerArg(&m_activeLayer);
+    AddArg("tolerance", 0, _("Distance tolerance for simplification."),
+           &m_opts.tolerance)
+        .SetRequired()
+        .SetMinValueIncluded(0);
+    AddArg("preserve-boundary", 0,
+           _("Whether the exterior boundary should be preserved."),
+           &m_opts.preserveBoundary);
+}
+
+#if defined HAVE_GEOS &&                                                       \
+    (GEOS_VERSION_MAJOR > 3 ||                                                 \
+     (GEOS_VERSION_MAJOR == 3 && GEOS_VERSION_MINOR >= 12))
+
+/** Class used by vector pipeline steps to create an output on-the-fly
+ * dataset where they can store on-the-fly layers.
+ */
+class GDALVectorGeomSimplifyCoverageOutputDataset final : public GDALDataset
+{
+  public:
+    GDALVectorGeomSimplifyCoverageOutputDataset(GDALDataset &srcDS);
+
+    void AddLayer(std::unique_ptr<OGRLayer> poNewLayer)
+    {
+        m_layers.push_back(std::move(poNewLayer));
+    }
+
+    int GetLayerCount() override
+    {
+        return static_cast<int>(m_layers.size());
+    }
+
+    OGRLayer *GetLayer(int idx) override
+    {
+        return m_layers[idx].get();
+    }
+
+    int TestCapability(const char *) override
+    {
+        // FIXME implement?
+        return false;
+    }
+
+    void ResetReading() override
+    {
+        m_srcDS.ResetReading();
+    }
+
+    OGRFeature *GetNextFeature(OGRLayer **, double *, GDALProgressFunc,
+                               void *) override
+    {
+        // FIXME implement?
+        CPLError(CE_Failure, CPLE_AppDefined,
+                 "GDALDataset::GetNextFeature not implemented.");
+        return nullptr;
+    }
+
+  private:
+    GDALDataset &m_srcDS;
+    std::vector<std::unique_ptr<OGRLayer>> m_layers{};
+};
+
+class GDALVectorGeomSimplifyCoverageAlgorithmLayer final : public OGRLayer
+{
+  public:
+    explicit GDALVectorGeomSimplifyCoverageAlgorithmLayer(
+        OGRLayer &src,
+        const GDALVectorGeomSimplifyCoverageAlgorithm::Options &opts)
+        : m_srcLayer(src), m_opts(opts)
+    {
+    }
+
+    ~GDALVectorGeomSimplifyCoverageAlgorithmLayer() override
+    {
+        if (m_GEOSContext != nullptr)
+        {
+            for (size_t i = 0; i < m_features.size(); i++)
+            {
+                GEOSGeom_destroy_r(m_GEOSContext, m_geos_result[i]);
+            }
+            GEOSFree_r(m_GEOSContext, m_geos_result);
+            finishGEOS_r(m_GEOSContext);
+        }
+    }
+
+    int TestCapability(const char *pszCap) override
+    {
+        if (EQUAL(pszCap, OLCRandomRead))
+        {
+            return true;
+        }
path: apps/gdalalg_vector_geom_simplify_coverage.cpp
user: login: rouault
body: ```suggestion
```
created_at: 2025-05-13T15:02:24Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,352 @@
+/******************************************************************************
+*
+ * Project:  GDAL
+ * Purpose:  "gdal vector geom simplify-coverage" subcommand
+ * Author:   Daniel Baston
+ *
+ ******************************************************************************
+ * Copyright (c) 2025, ISciences LLC
+ *
+ * SPDX-License-Identifier: MIT
+ ****************************************************************************/
+
+#include "gdalalg_vector_geom_simplify_coverage.h"
+
+#include "cpl_error.h"
+#include "gdal_priv.h"
+#include "gdalalg_vector_geom.h"
+#include "ogr_geometry.h"
+#include "ogr_geos.h"
+#include "ogrsf_frmts.h"
+
+#ifndef _
+#define _(x) (x)
+#endif
+
+//! @cond Doxygen_Suppress
+
+GDALVectorGeomSimplifyCoverageAlgorithm::
+    GDALVectorGeomSimplifyCoverageAlgorithm(bool standaloneStep)
+    : GDALVectorPipelineStepAlgorithm(NAME, DESCRIPTION, HELP_URL,
+                                      standaloneStep)
+{
+    AddActiveLayerArg(&m_activeLayer);
+    AddArg("tolerance", 0, _("Distance tolerance for simplification."),
+           &m_opts.tolerance)
+        .SetRequired()
+        .SetMinValueIncluded(0);
+    AddArg("preserve-boundary", 0,
+           _("Whether the exterior boundary should be preserved."),
+           &m_opts.preserveBoundary);
+}
+
+#if defined HAVE_GEOS &&                                                       \
+    (GEOS_VERSION_MAJOR > 3 ||                                                 \
+     (GEOS_VERSION_MAJOR == 3 && GEOS_VERSION_MINOR >= 12))
+
+/** Class used by vector pipeline steps to create an output on-the-fly
+ * dataset where they can store on-the-fly layers.
+ */
+class GDALVectorGeomSimplifyCoverageOutputDataset final : public GDALDataset
+{
+  public:
+    GDALVectorGeomSimplifyCoverageOutputDataset(GDALDataset &srcDS);
+
+    void AddLayer(std::unique_ptr<OGRLayer> poNewLayer)
+    {
+        m_layers.push_back(std::move(poNewLayer));
+    }
+
+    int GetLayerCount() override
+    {
+        return static_cast<int>(m_layers.size());
+    }
+
+    OGRLayer *GetLayer(int idx) override
+    {
+        return m_layers[idx].get();
+    }
+
+    int TestCapability(const char *) override
+    {
+        // FIXME implement?
+        return false;
+    }
+
+    void ResetReading() override
+    {
+        m_srcDS.ResetReading();
+    }
+
+    OGRFeature *GetNextFeature(OGRLayer **, double *, GDALProgressFunc,
+                               void *) override
+    {
+        // FIXME implement?
+        CPLError(CE_Failure, CPLE_AppDefined,
+                 "GDALDataset::GetNextFeature not implemented.");
+        return nullptr;
+    }
+
+  private:
+    GDALDataset &m_srcDS;
+    std::vector<std::unique_ptr<OGRLayer>> m_layers{};
+};
+
+class GDALVectorGeomSimplifyCoverageAlgorithmLayer final : public OGRLayer
+{
+  public:
+    explicit GDALVectorGeomSimplifyCoverageAlgorithmLayer(
+        OGRLayer &src,
+        const GDALVectorGeomSimplifyCoverageAlgorithm::Options &opts)
+        : m_srcLayer(src), m_opts(opts)
+    {
+    }
+
+    ~GDALVectorGeomSimplifyCoverageAlgorithmLayer() override
+    {
+        if (m_GEOSContext != nullptr)
+        {
+            for (size_t i = 0; i < m_features.size(); i++)
+            {
+                GEOSGeom_destroy_r(m_GEOSContext, m_geos_result[i]);
+            }
+            GEOSFree_r(m_GEOSContext, m_geos_result);
+            finishGEOS_r(m_GEOSContext);
+        }
+    }
+
+    int TestCapability(const char *pszCap) override
+    {
+        if (EQUAL(pszCap, OLCRandomRead))
+        {
+            return true;
+        }
+        return m_srcLayer.TestCapability(pszCap);
+    }
+
+    OGRErr IGetExtent(int iGeomField, OGREnvelope *psExtent,
+                      bool bForce) override
+    {
+        return m_srcLayer.GetExtent(iGeomField, psExtent, bForce);
+    }
+
+    OGRFeature *GetFeature(GIntBig) override
+    {
+        // FIXME implement?
+        CPLError(CE_Failure, CPLE_AppDefined, "Not implemented.");
+        return nullptr;
+    }
+
+    GIntBig GetFeatureCount(int bForce) override
+    {
+        return OGRLayer::GetFeatureCount(bForce);
+    }
+
path: apps/gdalalg_vector_geom_simplify_coverage.cpp
user: login: rouault
body: ```suggestion
```
created_at: 2025-05-13T15:02:42Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,70 @@
+/******************************************************************************
+*
+ * Project:  GDAL
+ * Purpose:  "gdal vector geom simplify-coverage"
+ * Author:   Daniel Baston
+ *
+ ******************************************************************************
+ * Copyright (c) 2025, ISciences LLC
+ *
+ * SPDX-License-Identifier: MIT
+ ****************************************************************************/
+
+#ifndef GDALALG_VECTOR_GEOM_SIMPLIFY_COVERAGE_INCLUDED
+#define GDALALG_VECTOR_GEOM_SIMPLIFY_COVERAGE_INCLUDED
+
+#include "gdalalg_vector_pipeline.h"
+#include "cpl_progress.h"
+
+#include <string>
+
+//! @cond Doxygen_Suppress
+
+/************************************************************************/
+/*                GDALVectorGeomSimplifyCoverageAlgorithm               */
+/************************************************************************/
+
+class GDALVectorGeomSimplifyCoverageAlgorithm
+    : public GDALVectorPipelineStepAlgorithm
+{
+  public:
+    static constexpr const char *NAME = "simplify-coverage";
+    static constexpr const char *DESCRIPTION =
+        "Simplify shared boundaries of a polygonal vector dataset.";
+    static constexpr const char *HELP_URL =
+        "/programs/gdal_vector_geom_simplify_coverage.html";
+
+    explicit GDALVectorGeomSimplifyCoverageAlgorithm(
+        bool standaloneStep = false);
+
+    struct Options
+    {
+        double tolerance;
+        bool preserveBoundary;
path: apps/gdalalg_vector_geom_simplify_coverage.h
user: login: rouault
body: ```suggestion
        double tolerance = 0;
        bool preserveBoundary = false;
```
created_at: 2025-05-13T20:06:51Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,91 @@
+.. _gdal_vector_geom_simplify_coverage:
+
+================================================================================
+``gdal vector geom simplify-coverage``
+================================================================================
+
+.. versionadded:: 3.12
+
+.. only:: html
+
+    Simplify shared boundaries of a polygonal vector dataset.
+
+.. Index:: gdal vector geom simplify-coverage
+
+Synopsis
+--------
+
+.. program-output:: gdal vector geom simplify-coverage --help-doc
+
+Description
+-----------
+
+:program:`gdal vector geom simplify-coverage` simplifies shared boundaries of a
+polygonal dataset.
+
path: doc/source/programs/gdal_vector_geom_simplify_coverage.rst
user: login: jratike80
body: Maybe the description could be improved. Actually also the outer boundary is simplified if not especially denied with `--preserve-external-boundary`. And what really is preserved is the topology of the coverage so that the shared boundaries remain shared, even they are not the same after simplification.
created_at: 2025-05-13T20:23:58Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,352 @@
+/******************************************************************************
+*
+ * Project:  GDAL
+ * Purpose:  "gdal vector geom simplify-coverage" subcommand
+ * Author:   Daniel Baston
+ *
+ ******************************************************************************
+ * Copyright (c) 2025, ISciences LLC
+ *
+ * SPDX-License-Identifier: MIT
+ ****************************************************************************/
+
+#include "gdalalg_vector_geom_simplify_coverage.h"
+
+#include "cpl_error.h"
+#include "gdal_priv.h"
+#include "gdalalg_vector_geom.h"
+#include "ogr_geometry.h"
+#include "ogr_geos.h"
+#include "ogrsf_frmts.h"
+
+#ifndef _
+#define _(x) (x)
+#endif
+
+//! @cond Doxygen_Suppress
+
+GDALVectorGeomSimplifyCoverageAlgorithm::
+    GDALVectorGeomSimplifyCoverageAlgorithm(bool standaloneStep)
+    : GDALVectorPipelineStepAlgorithm(NAME, DESCRIPTION, HELP_URL,
+                                      standaloneStep)
+{
+    AddActiveLayerArg(&m_activeLayer);
+    AddArg("tolerance", 0, _("Distance tolerance for simplification."),
+           &m_opts.tolerance)
+        .SetRequired()
+        .SetMinValueIncluded(0);
+    AddArg("preserve-boundary", 0,
+           _("Whether the exterior boundary should be preserved."),
+           &m_opts.preserveBoundary);
+}
+
+#if defined HAVE_GEOS &&                                                       \
+    (GEOS_VERSION_MAJOR > 3 ||                                                 \
+     (GEOS_VERSION_MAJOR == 3 && GEOS_VERSION_MINOR >= 12))
+
+/** Class used by vector pipeline steps to create an output on-the-fly
+ * dataset where they can store on-the-fly layers.
+ */
+class GDALVectorGeomSimplifyCoverageOutputDataset final : public GDALDataset
+{
+  public:
+    GDALVectorGeomSimplifyCoverageOutputDataset(GDALDataset &srcDS);
+
+    void AddLayer(std::unique_ptr<OGRLayer> poNewLayer)
+    {
+        m_layers.push_back(std::move(poNewLayer));
+    }
+
+    int GetLayerCount() override
+    {
+        return static_cast<int>(m_layers.size());
+    }
+
+    OGRLayer *GetLayer(int idx) override
+    {
+        return m_layers[idx].get();
+    }
+
+    int TestCapability(const char *) override
+    {
+        // FIXME implement?
+        return false;
+    }
+
+    void ResetReading() override
+    {
+        m_srcDS.ResetReading();
+    }
+
+    OGRFeature *GetNextFeature(OGRLayer **, double *, GDALProgressFunc,
+                               void *) override
+    {
+        // FIXME implement?
+        CPLError(CE_Failure, CPLE_AppDefined,
+                 "GDALDataset::GetNextFeature not implemented.");
+        return nullptr;
+    }
+
+  private:
+    GDALDataset &m_srcDS;
+    std::vector<std::unique_ptr<OGRLayer>> m_layers{};
+};
+
+class GDALVectorGeomSimplifyCoverageAlgorithmLayer final : public OGRLayer
+{
+  public:
+    explicit GDALVectorGeomSimplifyCoverageAlgorithmLayer(
+        OGRLayer &src,
+        const GDALVectorGeomSimplifyCoverageAlgorithm::Options &opts)
+        : m_srcLayer(src), m_opts(opts)
+    {
+    }
+
+    ~GDALVectorGeomSimplifyCoverageAlgorithmLayer() override
+    {
+        if (m_GEOSContext != nullptr)
+        {
+            for (size_t i = 0; i < m_features.size(); i++)
+            {
+                GEOSGeom_destroy_r(m_GEOSContext, m_geos_result[i]);
+            }
+            GEOSFree_r(m_GEOSContext, m_geos_result);
+            finishGEOS_r(m_GEOSContext);
+        }
+    }
+
+    int TestCapability(const char *pszCap) override
+    {
+        if (EQUAL(pszCap, OLCRandomRead))
+        {
+            return true;
+        }
+        return m_srcLayer.TestCapability(pszCap);
+    }
+
+    OGRErr IGetExtent(int iGeomField, OGREnvelope *psExtent,
+                      bool bForce) override
+    {
+        return m_srcLayer.GetExtent(iGeomField, psExtent, bForce);
+    }
+
+    OGRFeature *GetFeature(GIntBig) override
+    {
+        // FIXME implement?
+        CPLError(CE_Failure, CPLE_AppDefined, "Not implemented.");
+        return nullptr;
+    }
+
+    GIntBig GetFeatureCount(int bForce) override
+    {
+        return OGRLayer::GetFeatureCount(bForce);
+    }
+
path: apps/gdalalg_vector_geom_simplify_coverage.cpp
user: login: dbaston
body: removed
created_at: 2025-05-14T01:27:40Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,352 @@
+/******************************************************************************
+*
+ * Project:  GDAL
+ * Purpose:  "gdal vector geom simplify-coverage" subcommand
+ * Author:   Daniel Baston
+ *
+ ******************************************************************************
+ * Copyright (c) 2025, ISciences LLC
+ *
+ * SPDX-License-Identifier: MIT
+ ****************************************************************************/
+
+#include "gdalalg_vector_geom_simplify_coverage.h"
+
+#include "cpl_error.h"
+#include "gdal_priv.h"
+#include "gdalalg_vector_geom.h"
+#include "ogr_geometry.h"
+#include "ogr_geos.h"
+#include "ogrsf_frmts.h"
+
+#ifndef _
+#define _(x) (x)
+#endif
+
+//! @cond Doxygen_Suppress
+
+GDALVectorGeomSimplifyCoverageAlgorithm::
+    GDALVectorGeomSimplifyCoverageAlgorithm(bool standaloneStep)
+    : GDALVectorPipelineStepAlgorithm(NAME, DESCRIPTION, HELP_URL,
+                                      standaloneStep)
+{
+    AddActiveLayerArg(&m_activeLayer);
+    AddArg("tolerance", 0, _("Distance tolerance for simplification."),
+           &m_opts.tolerance)
+        .SetRequired()
+        .SetMinValueIncluded(0);
+    AddArg("preserve-boundary", 0,
+           _("Whether the exterior boundary should be preserved."),
+           &m_opts.preserveBoundary);
+}
+
+#if defined HAVE_GEOS &&                                                       \
+    (GEOS_VERSION_MAJOR > 3 ||                                                 \
+     (GEOS_VERSION_MAJOR == 3 && GEOS_VERSION_MINOR >= 12))
+
+/** Class used by vector pipeline steps to create an output on-the-fly
+ * dataset where they can store on-the-fly layers.
+ */
+class GDALVectorGeomSimplifyCoverageOutputDataset final : public GDALDataset
+{
+  public:
+    GDALVectorGeomSimplifyCoverageOutputDataset(GDALDataset &srcDS);
+
+    void AddLayer(std::unique_ptr<OGRLayer> poNewLayer)
+    {
+        m_layers.push_back(std::move(poNewLayer));
+    }
+
+    int GetLayerCount() override
+    {
+        return static_cast<int>(m_layers.size());
+    }
+
+    OGRLayer *GetLayer(int idx) override
+    {
+        return m_layers[idx].get();
+    }
+
+    int TestCapability(const char *) override
+    {
+        // FIXME implement?
+        return false;
+    }
+
+    void ResetReading() override
+    {
+        m_srcDS.ResetReading();
+    }
+
+    OGRFeature *GetNextFeature(OGRLayer **, double *, GDALProgressFunc,
+                               void *) override
+    {
+        // FIXME implement?
+        CPLError(CE_Failure, CPLE_AppDefined,
+                 "GDALDataset::GetNextFeature not implemented.");
+        return nullptr;
+    }
path: apps/gdalalg_vector_geom_simplify_coverage.cpp
user: login: dbaston
body: removed
created_at: 2025-05-14T01:27:56Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,70 @@
+/******************************************************************************
+*
+ * Project:  GDAL
+ * Purpose:  "gdal vector geom simplify-coverage"
+ * Author:   Daniel Baston
+ *
+ ******************************************************************************
+ * Copyright (c) 2025, ISciences LLC
+ *
+ * SPDX-License-Identifier: MIT
+ ****************************************************************************/
+
+#ifndef GDALALG_VECTOR_GEOM_SIMPLIFY_COVERAGE_INCLUDED
+#define GDALALG_VECTOR_GEOM_SIMPLIFY_COVERAGE_INCLUDED
+
+#include "gdalalg_vector_pipeline.h"
+#include "cpl_progress.h"
+
+#include <string>
+
+//! @cond Doxygen_Suppress
+
+/************************************************************************/
+/*                GDALVectorGeomSimplifyCoverageAlgorithm               */
+/************************************************************************/
+
+class GDALVectorGeomSimplifyCoverageAlgorithm
+    : public GDALVectorPipelineStepAlgorithm
+{
+  public:
+    static constexpr const char *NAME = "simplify-coverage";
+    static constexpr const char *DESCRIPTION =
+        "Simplify shared boundaries of a polygonal vector dataset.";
+    static constexpr const char *HELP_URL =
+        "/programs/gdal_vector_geom_simplify_coverage.html";
+
+    explicit GDALVectorGeomSimplifyCoverageAlgorithm(
+        bool standaloneStep = false);
+
+    struct Options
+    {
+        double tolerance;
+        bool preserveBoundary;
path: apps/gdalalg_vector_geom_simplify_coverage.h
user: login: dbaston
body: done
created_at: 2025-05-14T01:28:40Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,91 @@
+.. _gdal_vector_geom_simplify_coverage:
+
+================================================================================
+``gdal vector geom simplify-coverage``
+================================================================================
+
+.. versionadded:: 3.12
+
+.. only:: html
+
+    Simplify shared boundaries of a polygonal vector dataset.
+
+.. Index:: gdal vector geom simplify-coverage
+
+Synopsis
+--------
+
+.. program-output:: gdal vector geom simplify-coverage --help-doc
+
+Description
+-----------
+
+:program:`gdal vector geom simplify-coverage` simplifies shared boundaries of a
+polygonal dataset.
+
path: doc/source/programs/gdal_vector_geom_simplify_coverage.rst
user: login: dbaston
body: ... simplifies boundaries of a polygonal dataset, such that shared boundaries are preserved without introducing gaps or overlaps between features. Gaps or overlaps already present in the input dataset will not be corrected.

?
created_at: 2025-05-14T01:30:50Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,91 @@
+.. _gdal_vector_geom_simplify_coverage:
+
+================================================================================
+``gdal vector geom simplify-coverage``
+================================================================================
+
+.. versionadded:: 3.12
+
+.. only:: html
+
+    Simplify shared boundaries of a polygonal vector dataset.
+
+.. Index:: gdal vector geom simplify-coverage
+
+Synopsis
+--------
+
+.. program-output:: gdal vector geom simplify-coverage --help-doc
+
+Description
+-----------
+
+:program:`gdal vector geom simplify-coverage` simplifies shared boundaries of a
+polygonal dataset.
+
path: doc/source/programs/gdal_vector_geom_simplify_coverage.rst
user: login: jratike80
body: Feels good.
created_at: 2025-05-14T06:10:23Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,326 @@
+/******************************************************************************
+*
+ * Project:  GDAL
+ * Purpose:  "gdal vector geom simplify-coverage" subcommand
+ * Author:   Daniel Baston
+ *
+ ******************************************************************************
+ * Copyright (c) 2025, ISciences LLC
+ *
+ * SPDX-License-Identifier: MIT
+ ****************************************************************************/
+
+#include "gdalalg_vector_geom_simplify_coverage.h"
+
+#include "cpl_error.h"
+#include "gdal_priv.h"
+#include "gdalalg_vector_geom.h"
+#include "ogr_geometry.h"
+#include "ogr_geos.h"
+#include "ogrsf_frmts.h"
+
+#ifndef _
+#define _(x) (x)
+#endif
+
+//! @cond Doxygen_Suppress
+
+GDALVectorGeomSimplifyCoverageAlgorithm::
+    GDALVectorGeomSimplifyCoverageAlgorithm(bool standaloneStep)
+    : GDALVectorPipelineStepAlgorithm(NAME, DESCRIPTION, HELP_URL,
+                                      standaloneStep)
+{
+    AddActiveLayerArg(&m_activeLayer);
+    AddArg("tolerance", 0, _("Distance tolerance for simplification."),
+           &m_opts.tolerance)
+        .SetRequired()
+        .SetMinValueIncluded(0);
+    AddArg("preserve-boundary", 0,
+           _("Whether the exterior boundary should be preserved."),
+           &m_opts.preserveBoundary);
+}
+
+#if defined HAVE_GEOS &&                                                       \
+    (GEOS_VERSION_MAJOR > 3 ||                                                 \
+     (GEOS_VERSION_MAJOR == 3 && GEOS_VERSION_MINOR >= 12))
+
+/** Class used by vector pipeline steps to create an output on-the-fly
+ * dataset where they can store on-the-fly layers.
+ */
+class GDALVectorGeomSimplifyCoverageOutputDataset final : public GDALDataset
+{
+  public:
+    GDALVectorGeomSimplifyCoverageOutputDataset(GDALDataset &srcDS);
+
+    void AddLayer(std::unique_ptr<OGRLayer> poNewLayer)
+    {
+        m_layers.push_back(std::move(poNewLayer));
+    }
+
+    int GetLayerCount() override
+    {
+        return static_cast<int>(m_layers.size());
+    }
+
+    OGRLayer *GetLayer(int idx) override
+    {
+        return m_layers[idx].get();
+    }
+
+    int TestCapability(const char *) override
+    {
+        // FIXME implement?
+        return false;
+    }
+
+  private:
+    GDALDataset &m_srcDS;
path: apps/gdalalg_vector_geom_simplify_coverage.cpp
user: login: rouault
body: This member variable isn't used
created_at: 2025-05-14T08:15:10Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,352 @@
+/******************************************************************************
+*
+ * Project:  GDAL
+ * Purpose:  "gdal vector geom simplify-coverage" subcommand
+ * Author:   Daniel Baston
+ *
+ ******************************************************************************
+ * Copyright (c) 2025, ISciences LLC
+ *
+ * SPDX-License-Identifier: MIT
+ ****************************************************************************/
+
+#include "gdalalg_vector_geom_simplify_coverage.h"
+
+#include "cpl_error.h"
+#include "gdal_priv.h"
+#include "gdalalg_vector_geom.h"
+#include "ogr_geometry.h"
+#include "ogr_geos.h"
+#include "ogrsf_frmts.h"
+
+#ifndef _
+#define _(x) (x)
+#endif
+
+//! @cond Doxygen_Suppress
+
+GDALVectorGeomSimplifyCoverageAlgorithm::
+    GDALVectorGeomSimplifyCoverageAlgorithm(bool standaloneStep)
+    : GDALVectorPipelineStepAlgorithm(NAME, DESCRIPTION, HELP_URL,
+                                      standaloneStep)
+{
+    AddActiveLayerArg(&m_activeLayer);
+    AddArg("tolerance", 0, _("Distance tolerance for simplification."),
+           &m_opts.tolerance)
+        .SetRequired()
+        .SetMinValueIncluded(0);
+    AddArg("preserve-boundary", 0,
+           _("Whether the exterior boundary should be preserved."),
+           &m_opts.preserveBoundary);
+}
+
+#if defined HAVE_GEOS &&                                                       \
+    (GEOS_VERSION_MAJOR > 3 ||                                                 \
+     (GEOS_VERSION_MAJOR == 3 && GEOS_VERSION_MINOR >= 12))
+
+/** Class used by vector pipeline steps to create an output on-the-fly
+ * dataset where they can store on-the-fly layers.
+ */
+class GDALVectorGeomSimplifyCoverageOutputDataset final : public GDALDataset
+{
+  public:
+    GDALVectorGeomSimplifyCoverageOutputDataset(GDALDataset &srcDS);
+
+    void AddLayer(std::unique_ptr<OGRLayer> poNewLayer)
+    {
+        m_layers.push_back(std::move(poNewLayer));
+    }
+
+    int GetLayerCount() override
+    {
+        return static_cast<int>(m_layers.size());
+    }
+
+    OGRLayer *GetLayer(int idx) override
+    {
+        return m_layers[idx].get();
+    }
+
+    int TestCapability(const char *) override
+    {
+        // FIXME implement?
+        return false;
+    }
+
+    void ResetReading() override
+    {
+        m_srcDS.ResetReading();
+    }
+
+    OGRFeature *GetNextFeature(OGRLayer **, double *, GDALProgressFunc,
+                               void *) override
+    {
+        // FIXME implement?
+        CPLError(CE_Failure, CPLE_AppDefined,
+                 "GDALDataset::GetNextFeature not implemented.");
+        return nullptr;
+    }
+
+  private:
+    GDALDataset &m_srcDS;
+    std::vector<std::unique_ptr<OGRLayer>> m_layers{};
+};
+
+class GDALVectorGeomSimplifyCoverageAlgorithmLayer final : public OGRLayer
+{
+  public:
+    explicit GDALVectorGeomSimplifyCoverageAlgorithmLayer(
+        OGRLayer &src,
+        const GDALVectorGeomSimplifyCoverageAlgorithm::Options &opts)
+        : m_srcLayer(src), m_opts(opts)
+    {
+    }
+
+    ~GDALVectorGeomSimplifyCoverageAlgorithmLayer() override
+    {
+        if (m_GEOSContext != nullptr)
+        {
+            for (size_t i = 0; i < m_features.size(); i++)
+            {
+                GEOSGeom_destroy_r(m_GEOSContext, m_geos_result[i]);
+            }
+            GEOSFree_r(m_GEOSContext, m_geos_result);
+            finishGEOS_r(m_GEOSContext);
+        }
+    }
+
+    int TestCapability(const char *pszCap) override
+    {
+        if (EQUAL(pszCap, OLCRandomRead))
+        {
+            return true;
+        }
path: apps/gdalalg_vector_geom_simplify_coverage.cpp
user: login: rouault
body: If you keep OLCRandomRead, then you should implement GetFeature(), which you can actually implement by materializing and then returning m_features[idx]
created_at: 2025-05-15T15:25:34Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,316 @@
+/******************************************************************************
+*
+ * Project:  GDAL
+ * Purpose:  "gdal vector geom simplify-coverage" subcommand
+ * Author:   Daniel Baston
+ *
+ ******************************************************************************
+ * Copyright (c) 2025, ISciences LLC
+ *
+ * SPDX-License-Identifier: MIT
+ ****************************************************************************/
+
+#include "gdalalg_vector_geom_simplify_coverage.h"
+
+#include "cpl_error.h"
+#include "gdal_priv.h"
+#include "gdalalg_vector_geom.h"
+#include "ogr_geometry.h"
+#include "ogr_geos.h"
+#include "ogrsf_frmts.h"
+
+#ifndef _
+#define _(x) (x)
+#endif
+
+//! @cond Doxygen_Suppress
+
+GDALVectorGeomSimplifyCoverageAlgorithm::
+    GDALVectorGeomSimplifyCoverageAlgorithm(bool standaloneStep)
+    : GDALVectorPipelineStepAlgorithm(NAME, DESCRIPTION, HELP_URL,
+                                      standaloneStep)
+{
+    AddActiveLayerArg(&m_activeLayer);
+    AddArg("tolerance", 0, _("Distance tolerance for simplification."),
+           &m_opts.tolerance)
+        .SetRequired()
+        .SetMinValueIncluded(0);
+    AddArg("preserve-boundary", 0,
+           _("Whether the exterior boundary should be preserved."),
+           &m_opts.preserveBoundary);
+}
+
+#if defined HAVE_GEOS &&                                                       \
+    (GEOS_VERSION_MAJOR > 3 ||                                                 \
+     (GEOS_VERSION_MAJOR == 3 && GEOS_VERSION_MINOR >= 12))
+
+/**
+ * Dataset used to read all input features into memory and simplify their
+ * geometries.
+ */
+class GDALVectorGeomSimplifyCoverageOutputDataset final : public GDALDataset
+{
+  public:
+    GDALVectorGeomSimplifyCoverageOutputDataset();
+
+    void AddLayer(std::unique_ptr<OGRLayer> poNewLayer)
+    {
+        m_layers.push_back(std::move(poNewLayer));
+    }
+
+    int GetLayerCount() override
+    {
+        return static_cast<int>(m_layers.size());
+    }
+
+    OGRLayer *GetLayer(int idx) override
+    {
+        return m_layers[idx].get();
+    }
+
+  private:
+    std::vector<std::unique_ptr<OGRLayer>> m_layers{};
+};
+
+class GDALVectorGeomSimplifyCoverageAlgorithmLayer final : public OGRLayer
+{
+  public:
+    explicit GDALVectorGeomSimplifyCoverageAlgorithmLayer(
+        OGRLayer &src,
+        const GDALVectorGeomSimplifyCoverageAlgorithm::Options &opts)
+        : m_srcLayer(src), m_opts(opts)
+    {
+    }
+
+    ~GDALVectorGeomSimplifyCoverageAlgorithmLayer() override
+    {
+        if (m_GEOSContext != nullptr)
+        {
+            for (size_t i = 0; i < m_features.size(); i++)
+            {
+                GEOSGeom_destroy_r(m_GEOSContext, m_geos_result[i]);
+            }
+            GEOSFree_r(m_GEOSContext, m_geos_result);
+            finishGEOS_r(m_GEOSContext);
+        }
+    }
+
+    int TestCapability(const char *pszCap) override
+    {
+        if (EQUAL(pszCap, OLCRandomRead))
+        {
+            return true;
+        }
+        return m_srcLayer.TestCapability(pszCap);
+    }
+
+    OGRErr IGetExtent(int iGeomField, OGREnvelope *psExtent,
+                      bool bForce) override
+    {
+        return m_srcLayer.GetExtent(iGeomField, psExtent, bForce);
+    }
+
+    OGRFeatureDefn *GetLayerDefn() override
+    {
+        return m_srcLayer.GetLayerDefn();
+    }
+
+    void ResetReading() override
+    {
+        if (!m_materialized)
+        {
+            m_srcLayer.ResetReading();
+            m_features.clear();
+        }
+        m_pos = 0;
+    }
+
+    OGRFeature *GetNextFeature() override
+    {
+        if (!m_materialized)
+        {
+            if (!Materialize())
+            {
+                return nullptr;
+            }
+        }
+
+        if (m_pos >= m_features.size())
+        {
+            return nullptr;
+        }
+
+        auto &ret = m_features[m_pos];
+        m_pos++;
+
+        return ret.release();
path: apps/gdalalg_vector_geom_simplify_coverage.cpp
user: login: rouault
body: I suspect that given you relese m_features[m_pos], if you do ResetReading() and iterate again, you'll get a null feature. Perhaps you should return ret->Clone() instead ?

To check your implementation of OGRLayer is conformant, you could run test_ogrsf on a .gdalg file. See test_gdalalg_vector_clip_test_ogrsf() for example
created_at: 2025-05-15T15:27:33Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,316 @@
+/******************************************************************************
+*
+ * Project:  GDAL
+ * Purpose:  "gdal vector geom simplify-coverage" subcommand
+ * Author:   Daniel Baston
+ *
+ ******************************************************************************
+ * Copyright (c) 2025, ISciences LLC
+ *
+ * SPDX-License-Identifier: MIT
+ ****************************************************************************/
+
+#include "gdalalg_vector_geom_simplify_coverage.h"
+
+#include "cpl_error.h"
+#include "gdal_priv.h"
+#include "gdalalg_vector_geom.h"
+#include "ogr_geometry.h"
+#include "ogr_geos.h"
+#include "ogrsf_frmts.h"
+
+#ifndef _
+#define _(x) (x)
+#endif
+
+//! @cond Doxygen_Suppress
+
+GDALVectorGeomSimplifyCoverageAlgorithm::
+    GDALVectorGeomSimplifyCoverageAlgorithm(bool standaloneStep)
+    : GDALVectorPipelineStepAlgorithm(NAME, DESCRIPTION, HELP_URL,
+                                      standaloneStep)
+{
+    AddActiveLayerArg(&m_activeLayer);
+    AddArg("tolerance", 0, _("Distance tolerance for simplification."),
+           &m_opts.tolerance)
+        .SetRequired()
+        .SetMinValueIncluded(0);
+    AddArg("preserve-boundary", 0,
+           _("Whether the exterior boundary should be preserved."),
+           &m_opts.preserveBoundary);
+}
+
+#if defined HAVE_GEOS &&                                                       \
+    (GEOS_VERSION_MAJOR > 3 ||                                                 \
+     (GEOS_VERSION_MAJOR == 3 && GEOS_VERSION_MINOR >= 12))
+
+/**
+ * Dataset used to read all input features into memory and simplify their
+ * geometries.
+ */
+class GDALVectorGeomSimplifyCoverageOutputDataset final : public GDALDataset
+{
+  public:
+    GDALVectorGeomSimplifyCoverageOutputDataset();
+
+    void AddLayer(std::unique_ptr<OGRLayer> poNewLayer)
+    {
+        m_layers.push_back(std::move(poNewLayer));
+    }
+
+    int GetLayerCount() override
+    {
+        return static_cast<int>(m_layers.size());
+    }
+
+    OGRLayer *GetLayer(int idx) override
+    {
+        return m_layers[idx].get();
+    }
+
+  private:
+    std::vector<std::unique_ptr<OGRLayer>> m_layers{};
+};
+
+class GDALVectorGeomSimplifyCoverageAlgorithmLayer final : public OGRLayer
+{
+  public:
+    explicit GDALVectorGeomSimplifyCoverageAlgorithmLayer(
+        OGRLayer &src,
+        const GDALVectorGeomSimplifyCoverageAlgorithm::Options &opts)
+        : m_srcLayer(src), m_opts(opts)
+    {
+    }
+
+    ~GDALVectorGeomSimplifyCoverageAlgorithmLayer() override
+    {
+        if (m_GEOSContext != nullptr)
+        {
+            for (size_t i = 0; i < m_features.size(); i++)
+            {
+                GEOSGeom_destroy_r(m_GEOSContext, m_geos_result[i]);
+            }
+            GEOSFree_r(m_GEOSContext, m_geos_result);
+            finishGEOS_r(m_GEOSContext);
+        }
+    }
+
+    int TestCapability(const char *pszCap) override
+    {
+        if (EQUAL(pszCap, OLCRandomRead))
+        {
+            return true;
+        }
+        return m_srcLayer.TestCapability(pszCap);
+    }
+
+    OGRErr IGetExtent(int iGeomField, OGREnvelope *psExtent,
+                      bool bForce) override
+    {
+        return m_srcLayer.GetExtent(iGeomField, psExtent, bForce);
+    }
+
+    OGRFeatureDefn *GetLayerDefn() override
+    {
+        return m_srcLayer.GetLayerDefn();
+    }
+
+    void ResetReading() override
+    {
+        if (!m_materialized)
+        {
+            m_srcLayer.ResetReading();
+            m_features.clear();
+        }
+        m_pos = 0;
+    }
+
+    OGRFeature *GetNextFeature() override
path: apps/gdalalg_vector_geom_simplify_coverage.cpp
user: login: rouault
body: your implementation ignores potential spatial and attribute filters that might have been set by later stages. You should probably rename your method as GetNextRawFeature(), and make the class derives from OGRGetNextFeatureThroughRaw<GDALVectorGeomSimplifyCoverageAlgorithmLayer> (in addition of OGRLayer)
created_at: 2025-05-15T15:32:21Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,316 @@
+/******************************************************************************
+*
+ * Project:  GDAL
+ * Purpose:  "gdal vector geom simplify-coverage" subcommand
+ * Author:   Daniel Baston
+ *
+ ******************************************************************************
+ * Copyright (c) 2025, ISciences LLC
+ *
+ * SPDX-License-Identifier: MIT
+ ****************************************************************************/
+
+#include "gdalalg_vector_geom_simplify_coverage.h"
+
+#include "cpl_error.h"
+#include "gdal_priv.h"
+#include "gdalalg_vector_geom.h"
+#include "ogr_geometry.h"
+#include "ogr_geos.h"
+#include "ogrsf_frmts.h"
+
+#ifndef _
+#define _(x) (x)
+#endif
+
+//! @cond Doxygen_Suppress
+
+GDALVectorGeomSimplifyCoverageAlgorithm::
+    GDALVectorGeomSimplifyCoverageAlgorithm(bool standaloneStep)
+    : GDALVectorPipelineStepAlgorithm(NAME, DESCRIPTION, HELP_URL,
+                                      standaloneStep)
+{
+    AddActiveLayerArg(&m_activeLayer);
+    AddArg("tolerance", 0, _("Distance tolerance for simplification."),
+           &m_opts.tolerance)
+        .SetRequired()
+        .SetMinValueIncluded(0);
+    AddArg("preserve-boundary", 0,
+           _("Whether the exterior boundary should be preserved."),
+           &m_opts.preserveBoundary);
+}
+
+#if defined HAVE_GEOS &&                                                       \
+    (GEOS_VERSION_MAJOR > 3 ||                                                 \
+     (GEOS_VERSION_MAJOR == 3 && GEOS_VERSION_MINOR >= 12))
+
+/**
+ * Dataset used to read all input features into memory and simplify their
+ * geometries.
+ */
+class GDALVectorGeomSimplifyCoverageOutputDataset final : public GDALDataset
+{
+  public:
+    GDALVectorGeomSimplifyCoverageOutputDataset();
+
+    void AddLayer(std::unique_ptr<OGRLayer> poNewLayer)
+    {
+        m_layers.push_back(std::move(poNewLayer));
+    }
+
+    int GetLayerCount() override
+    {
+        return static_cast<int>(m_layers.size());
+    }
+
+    OGRLayer *GetLayer(int idx) override
+    {
+        return m_layers[idx].get();
+    }
+
+  private:
+    std::vector<std::unique_ptr<OGRLayer>> m_layers{};
+};
+
+class GDALVectorGeomSimplifyCoverageAlgorithmLayer final : public OGRLayer
+{
+  public:
+    explicit GDALVectorGeomSimplifyCoverageAlgorithmLayer(
+        OGRLayer &src,
+        const GDALVectorGeomSimplifyCoverageAlgorithm::Options &opts)
+        : m_srcLayer(src), m_opts(opts)
+    {
+    }
+
+    ~GDALVectorGeomSimplifyCoverageAlgorithmLayer() override
+    {
+        if (m_GEOSContext != nullptr)
+        {
+            for (size_t i = 0; i < m_features.size(); i++)
+            {
+                GEOSGeom_destroy_r(m_GEOSContext, m_geos_result[i]);
+            }
+            GEOSFree_r(m_GEOSContext, m_geos_result);
+            finishGEOS_r(m_GEOSContext);
+        }
+    }
+
+    int TestCapability(const char *pszCap) override
+    {
+        if (EQUAL(pszCap, OLCRandomRead))
+        {
+            return true;
+        }
+        return m_srcLayer.TestCapability(pszCap);
+    }
+
+    OGRErr IGetExtent(int iGeomField, OGREnvelope *psExtent,
+                      bool bForce) override
+    {
+        return m_srcLayer.GetExtent(iGeomField, psExtent, bForce);
+    }
+
+    OGRFeatureDefn *GetLayerDefn() override
+    {
+        return m_srcLayer.GetLayerDefn();
+    }
+
+    void ResetReading() override
+    {
+        if (!m_materialized)
+        {
+            m_srcLayer.ResetReading();
+            m_features.clear();
+        }
+        m_pos = 0;
+    }
+
+    OGRFeature *GetNextFeature() override
path: apps/gdalalg_vector_geom_simplify_coverage.cpp
user: login: dbaston
body: Maybe it's better to go back to just creating a dataset with the MEM driver? Do you see a downside to that?
created_at: 2025-05-16T17:53:04Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,316 @@
+/******************************************************************************
+*
+ * Project:  GDAL
+ * Purpose:  "gdal vector geom simplify-coverage" subcommand
+ * Author:   Daniel Baston
+ *
+ ******************************************************************************
+ * Copyright (c) 2025, ISciences LLC
+ *
+ * SPDX-License-Identifier: MIT
+ ****************************************************************************/
+
+#include "gdalalg_vector_geom_simplify_coverage.h"
+
+#include "cpl_error.h"
+#include "gdal_priv.h"
+#include "gdalalg_vector_geom.h"
+#include "ogr_geometry.h"
+#include "ogr_geos.h"
+#include "ogrsf_frmts.h"
+
+#ifndef _
+#define _(x) (x)
+#endif
+
+//! @cond Doxygen_Suppress
+
+GDALVectorGeomSimplifyCoverageAlgorithm::
+    GDALVectorGeomSimplifyCoverageAlgorithm(bool standaloneStep)
+    : GDALVectorPipelineStepAlgorithm(NAME, DESCRIPTION, HELP_URL,
+                                      standaloneStep)
+{
+    AddActiveLayerArg(&m_activeLayer);
+    AddArg("tolerance", 0, _("Distance tolerance for simplification."),
+           &m_opts.tolerance)
+        .SetRequired()
+        .SetMinValueIncluded(0);
+    AddArg("preserve-boundary", 0,
+           _("Whether the exterior boundary should be preserved."),
+           &m_opts.preserveBoundary);
+}
+
+#if defined HAVE_GEOS &&                                                       \
+    (GEOS_VERSION_MAJOR > 3 ||                                                 \
+     (GEOS_VERSION_MAJOR == 3 && GEOS_VERSION_MINOR >= 12))
+
+/**
+ * Dataset used to read all input features into memory and simplify their
+ * geometries.
+ */
+class GDALVectorGeomSimplifyCoverageOutputDataset final : public GDALDataset
+{
+  public:
+    GDALVectorGeomSimplifyCoverageOutputDataset();
+
+    void AddLayer(std::unique_ptr<OGRLayer> poNewLayer)
+    {
+        m_layers.push_back(std::move(poNewLayer));
+    }
+
+    int GetLayerCount() override
+    {
+        return static_cast<int>(m_layers.size());
+    }
+
+    OGRLayer *GetLayer(int idx) override
+    {
+        return m_layers[idx].get();
+    }
+
+  private:
+    std::vector<std::unique_ptr<OGRLayer>> m_layers{};
+};
+
+class GDALVectorGeomSimplifyCoverageAlgorithmLayer final : public OGRLayer
+{
+  public:
+    explicit GDALVectorGeomSimplifyCoverageAlgorithmLayer(
+        OGRLayer &src,
+        const GDALVectorGeomSimplifyCoverageAlgorithm::Options &opts)
+        : m_srcLayer(src), m_opts(opts)
+    {
+    }
+
+    ~GDALVectorGeomSimplifyCoverageAlgorithmLayer() override
+    {
+        if (m_GEOSContext != nullptr)
+        {
+            for (size_t i = 0; i < m_features.size(); i++)
+            {
+                GEOSGeom_destroy_r(m_GEOSContext, m_geos_result[i]);
+            }
+            GEOSFree_r(m_GEOSContext, m_geos_result);
+            finishGEOS_r(m_GEOSContext);
+        }
+    }
+
+    int TestCapability(const char *pszCap) override
+    {
+        if (EQUAL(pszCap, OLCRandomRead))
+        {
+            return true;
+        }
+        return m_srcLayer.TestCapability(pszCap);
+    }
+
+    OGRErr IGetExtent(int iGeomField, OGREnvelope *psExtent,
+                      bool bForce) override
+    {
+        return m_srcLayer.GetExtent(iGeomField, psExtent, bForce);
+    }
+
+    OGRFeatureDefn *GetLayerDefn() override
+    {
+        return m_srcLayer.GetLayerDefn();
+    }
+
+    void ResetReading() override
+    {
+        if (!m_materialized)
+        {
+            m_srcLayer.ResetReading();
+            m_features.clear();
+        }
+        m_pos = 0;
+    }
+
+    OGRFeature *GetNextFeature() override
path: apps/gdalalg_vector_geom_simplify_coverage.cpp
user: login: rouault
body: > Maybe it's better to go back to just creating a dataset with the MEM driver?

that could be more convenient indeed
created_at: 2025-05-16T17:59:51Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,220 @@
+/******************************************************************************
+*
+ * Project:  GDAL
+ * Purpose:  "gdal vector geom simplify-coverage" subcommand
+ * Author:   Daniel Baston
+ *
+ ******************************************************************************
+ * Copyright (c) 2025, ISciences LLC
+ *
+ * SPDX-License-Identifier: MIT
+ ****************************************************************************/
+
+#include "gdalalg_vector_geom_simplify_coverage.h"
+
+#include "cpl_error.h"
+#include "gdal_priv.h"
+#include "gdalalg_vector_geom.h"
+#include "ogr_geometry.h"
+#include "ogr_geos.h"
+#include "ogrsf_frmts.h"
+
+#ifndef _
+#define _(x) (x)
+#endif
+
+//! @cond Doxygen_Suppress
+
+GDALVectorGeomSimplifyCoverageAlgorithm::
+    GDALVectorGeomSimplifyCoverageAlgorithm(bool standaloneStep)
+    : GDALVectorPipelineStepAlgorithm(NAME, DESCRIPTION, HELP_URL,
+                                      standaloneStep)
+{
+    AddActiveLayerArg(&m_activeLayer);
+    AddArg("tolerance", 0, _("Distance tolerance for simplification."),
+           &m_opts.tolerance)
+        .SetRequired()
+        .SetMinValueIncluded(0);
+    AddArg("preserve-boundary", 0,
+           _("Whether the exterior boundary should be preserved."),
+           &m_opts.preserveBoundary);
+}
+
+#if defined HAVE_GEOS &&                                                       \
+    (GEOS_VERSION_MAJOR > 3 ||                                                 \
+     (GEOS_VERSION_MAJOR == 3 && GEOS_VERSION_MINOR >= 12))
+
+class GDALVectorGeomSimplifyCoverageOutputDataset
+    : public GDALVectorNonStreamingAlgorithmDataset
+{
+  public:
+    GDALVectorGeomSimplifyCoverageOutputDataset(
+        const GDALVectorGeomSimplifyCoverageAlgorithm::Options &opts)
+        : m_opts(opts)
+    {
+    }
+
+    ~GDALVectorGeomSimplifyCoverageOutputDataset()
+    {
+        if (m_poGeosContext != nullptr)
+        {
+            for (size_t i = 0; i < m_nGeosResultSize; i++)
+            {
+                GEOSGeom_destroy_r(m_poGeosContext, m_papoGeosResults[i]);
+            }
+            GEOSFree_r(m_poGeosContext, m_papoGeosResults);
+            finishGEOS_r(m_poGeosContext);
+        }
+    }
+
+    bool Process(OGRLayer &srcLayer, OGRLayer &dstLayer) override
+    {
+        std::vector<OGRFeatureUniquePtr> features;
+        std::vector<GEOSGeometry *> geoms;
+        m_poGeosContext = OGRGeometry::createGEOSContext();
+
+        // Copy features from srcLayer into dstLayer, converting
+        // their geometries to GEOS
+        for (auto &feature : srcLayer)
+        {
+            const OGRGeometry *fgeom = feature->GetGeometryRef();
+
+            // Avoid segfault with non-polygonal inputs on GEOS < 3.12.2
+            // Later versions produce an error instead
+            if (fgeom == nullptr || fgeom->getDimension() != 2)
+            {
+                for (auto &geom : geoms)
+                {
+                    GEOSGeom_destroy_r(m_poGeosContext, geom);
+                }
+                CPLError(CE_Failure, CPLE_AppDefined,
+                         "Coverage simplification can only be performed on "
+                         "polygonal geometries.");
+                return false;
+            }
+
+            geoms.push_back(fgeom->exportToGEOS(m_poGeosContext, false));
+
+            feature->SetGeometry(nullptr);  // free some memory
+            feature->SetFDefnUnsafe(dstLayer.GetLayerDefn());
+
+            features.push_back(std::move(feature));
+        }
+
+        // Perform coverage simplifciation
+        GEOSGeometry *coll = GEOSGeom_createCollection_r(
+            m_poGeosContext, GEOS_GEOMETRYCOLLECTION, geoms.data(),
+            static_cast<unsigned int>(geoms.size()));
+
+        if (coll == nullptr)
+        {
+            for (auto &geom : geoms)
+            {
+                GEOSGeom_destroy_r(m_poGeosContext, geom);
+            }
+            return false;
+        }
+
+        GEOSGeometry *geos_result = GEOSCoverageSimplifyVW_r(
+            m_poGeosContext, coll, m_opts.tolerance, m_opts.preserveBoundary);
+        GEOSGeom_destroy_r(m_poGeosContext, coll);
+
+        if (geos_result == nullptr)
+        {
+            return false;
+        }
+
+        m_papoGeosResults = GEOSGeom_releaseCollection_r(
+            m_poGeosContext, geos_result, &m_nGeosResultSize);
+        GEOSGeom_destroy_r(m_poGeosContext, geos_result);
+        CPLAssert(features.size() == m_nGeosResultSize);
+
+        // Create features with the modified geometries
+        for (size_t i = 0; i < features.size(); i++)
+        {
+            GEOSGeometry *dstGeom = m_papoGeosResults[i];
+
+            std::unique_ptr<OGRGeometry> poSimplified(
+                OGRGeometryFactory::createFromGEOS(m_poGeosContext, dstGeom));
+            GEOSGeom_destroy_r(m_poGeosContext, m_papoGeosResults[i]);
+            m_papoGeosResults[i] = nullptr;
+
+            if (poSimplified == nullptr)
+            {
+                CPLError(CE_Failure, CPLE_AppDefined,
+                         "Failed to convert result from GEOS");
+                return false;
+            }
+            poSimplified->assignSpatialReference(
+                dstLayer.GetLayerDefn()->GetGeomFieldDefn(0)->GetSpatialRef());
+            features[i]->SetGeometry(std::move(poSimplified));
+
+            if (dstLayer.CreateFeature(features[i].release()) != CE_None)
+            {
+                return false;
+            }
path: apps/gdalalg_vector_geom_simplify_coverage.cpp
user: login: rouault
body: Using release() will leak as CreateFeature(), perhaps unexpectingly doesn't take ownership of the passed feature.

```suggestion
            if (dstLayer.CreateFeature(features[i].get()) != CE_None)
            {
                return false;
            }
            features[i].reset();
```
created_at: 2025-05-18T01:31:13Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,220 @@
+/******************************************************************************
+*
+ * Project:  GDAL
+ * Purpose:  "gdal vector geom simplify-coverage" subcommand
+ * Author:   Daniel Baston
+ *
+ ******************************************************************************
+ * Copyright (c) 2025, ISciences LLC
+ *
+ * SPDX-License-Identifier: MIT
+ ****************************************************************************/
+
+#include "gdalalg_vector_geom_simplify_coverage.h"
+
+#include "cpl_error.h"
+#include "gdal_priv.h"
+#include "gdalalg_vector_geom.h"
+#include "ogr_geometry.h"
+#include "ogr_geos.h"
+#include "ogrsf_frmts.h"
+
+#ifndef _
+#define _(x) (x)
+#endif
+
+//! @cond Doxygen_Suppress
+
+GDALVectorGeomSimplifyCoverageAlgorithm::
+    GDALVectorGeomSimplifyCoverageAlgorithm(bool standaloneStep)
+    : GDALVectorPipelineStepAlgorithm(NAME, DESCRIPTION, HELP_URL,
+                                      standaloneStep)
+{
+    AddActiveLayerArg(&m_activeLayer);
+    AddArg("tolerance", 0, _("Distance tolerance for simplification."),
+           &m_opts.tolerance)
+        .SetRequired()
+        .SetMinValueIncluded(0);
+    AddArg("preserve-boundary", 0,
+           _("Whether the exterior boundary should be preserved."),
+           &m_opts.preserveBoundary);
+}
+
+#if defined HAVE_GEOS &&                                                       \
+    (GEOS_VERSION_MAJOR > 3 ||                                                 \
+     (GEOS_VERSION_MAJOR == 3 && GEOS_VERSION_MINOR >= 12))
+
+class GDALVectorGeomSimplifyCoverageOutputDataset
+    : public GDALVectorNonStreamingAlgorithmDataset
+{
+  public:
+    GDALVectorGeomSimplifyCoverageOutputDataset(
+        const GDALVectorGeomSimplifyCoverageAlgorithm::Options &opts)
+        : m_opts(opts)
+    {
+    }
+
+    ~GDALVectorGeomSimplifyCoverageOutputDataset()
+    {
+        if (m_poGeosContext != nullptr)
+        {
+            for (size_t i = 0; i < m_nGeosResultSize; i++)
+            {
+                GEOSGeom_destroy_r(m_poGeosContext, m_papoGeosResults[i]);
+            }
+            GEOSFree_r(m_poGeosContext, m_papoGeosResults);
+            finishGEOS_r(m_poGeosContext);
+        }
+    }
+
+    bool Process(OGRLayer &srcLayer, OGRLayer &dstLayer) override
+    {
+        std::vector<OGRFeatureUniquePtr> features;
+        std::vector<GEOSGeometry *> geoms;
+        m_poGeosContext = OGRGeometry::createGEOSContext();
+
+        // Copy features from srcLayer into dstLayer, converting
+        // their geometries to GEOS
+        for (auto &feature : srcLayer)
+        {
+            const OGRGeometry *fgeom = feature->GetGeometryRef();
+
+            // Avoid segfault with non-polygonal inputs on GEOS < 3.12.2
+            // Later versions produce an error instead
+            if (fgeom == nullptr || fgeom->getDimension() != 2)
+            {
+                for (auto &geom : geoms)
+                {
+                    GEOSGeom_destroy_r(m_poGeosContext, geom);
+                }
+                CPLError(CE_Failure, CPLE_AppDefined,
+                         "Coverage simplification can only be performed on "
+                         "polygonal geometries.");
+                return false;
+            }
+
+            geoms.push_back(fgeom->exportToGEOS(m_poGeosContext, false));
+
+            feature->SetGeometry(nullptr);  // free some memory
+            feature->SetFDefnUnsafe(dstLayer.GetLayerDefn());
+
+            features.push_back(std::move(feature));
+        }
+
+        // Perform coverage simplifciation
+        GEOSGeometry *coll = GEOSGeom_createCollection_r(
+            m_poGeosContext, GEOS_GEOMETRYCOLLECTION, geoms.data(),
+            static_cast<unsigned int>(geoms.size()));
+
+        if (coll == nullptr)
+        {
+            for (auto &geom : geoms)
+            {
+                GEOSGeom_destroy_r(m_poGeosContext, geom);
+            }
+            return false;
+        }
+
+        GEOSGeometry *geos_result = GEOSCoverageSimplifyVW_r(
+            m_poGeosContext, coll, m_opts.tolerance, m_opts.preserveBoundary);
+        GEOSGeom_destroy_r(m_poGeosContext, coll);
+
+        if (geos_result == nullptr)
+        {
+            return false;
+        }
+
+        m_papoGeosResults = GEOSGeom_releaseCollection_r(
+            m_poGeosContext, geos_result, &m_nGeosResultSize);
+        GEOSGeom_destroy_r(m_poGeosContext, geos_result);
+        CPLAssert(features.size() == m_nGeosResultSize);
+
+        // Create features with the modified geometries
+        for (size_t i = 0; i < features.size(); i++)
+        {
+            GEOSGeometry *dstGeom = m_papoGeosResults[i];
+
+            std::unique_ptr<OGRGeometry> poSimplified(
+                OGRGeometryFactory::createFromGEOS(m_poGeosContext, dstGeom));
+            GEOSGeom_destroy_r(m_poGeosContext, m_papoGeosResults[i]);
+            m_papoGeosResults[i] = nullptr;
+
+            if (poSimplified == nullptr)
+            {
+                CPLError(CE_Failure, CPLE_AppDefined,
+                         "Failed to convert result from GEOS");
+                return false;
+            }
+            poSimplified->assignSpatialReference(
+                dstLayer.GetLayerDefn()->GetGeomFieldDefn(0)->GetSpatialRef());
+            features[i]->SetGeometry(std::move(poSimplified));
+
+            if (dstLayer.CreateFeature(features[i].release()) != CE_None)
+            {
+                return false;
+            }
path: apps/gdalalg_vector_geom_simplify_coverage.cpp
user: login: dbaston
body: Fixed, thanks
created_at: 2025-05-18T01:39:15Z
author_association: MEMBER
diff_hunk: @@ -219,6 +221,76 @@ class GDALVectorPipelinePassthroughLayer /* non final */
     }
 };
 
+/**
+ * Dataset used to read all input features into memory and perform some
+ * processing.
+ */
+class GDALVectorNonStreamingAlgorithmDataset : public GDALDataset
+{
+  public:
+    GDALVectorNonStreamingAlgorithmDataset()
+    {
+        GDALDriver *poMemDriver =
+            GetGDALDriverManager()->GetDriverByName("MEM");
+        m_ds.reset(cpl::down_cast<MEMDataset *>(
+            poMemDriver->Create("", 0, 0, 0, GDT_Unknown, nullptr)));
+    }
+
+    virtual bool Process(OGRLayer &srcLayer, OGRLayer &dstLayer) = 0;
+
+    bool AddProcessedLayer(OGRLayer &srcLayer)
+    {
+        OGRMemLayer *poDstLayer = cpl::down_cast<OGRMemLayer *>(
path: apps/gdalalg_vector_pipeline.h
user: login: rouault
body: We could test srcLayer.TestCapability(OLCStringsAsUTF8), and if set pass the ADVERTIZE_UTF8=YES layer creation option
created_at: 2025-05-18T12:46:57Z
author_association: MEMBER
diff_hunk: @@ -135,6 +135,9 @@ class CPL_DLL MEMDataset CPL_NON_FINAL : public GDALDataset
 
     OGRLayer *GetLayer(int) override;
 
+    OGRLayer *CreateLayer(const OGRFeatureDefn &oDefn,
path: frmts/mem/memdataset.h
user: login: rouault
body: if you return a OGRMemLayer* you could save the cpl::down_cast<> in gdalalg_vector_geom_simplify_coverage.cpp
created_at: 2025-05-18T12:48:04Z
author_association: MEMBER
diff_hunk: @@ -219,6 +221,76 @@ class GDALVectorPipelinePassthroughLayer /* non final */
     }
 };
 
+/**
+ * Dataset used to read all input features into memory and perform some
+ * processing.
+ */
+class GDALVectorNonStreamingAlgorithmDataset : public GDALDataset
+{
+  public:
+    GDALVectorNonStreamingAlgorithmDataset()
+    {
+        GDALDriver *poMemDriver =
+            GetGDALDriverManager()->GetDriverByName("MEM");
+        m_ds.reset(cpl::down_cast<MEMDataset *>(
+            poMemDriver->Create("", 0, 0, 0, GDT_Unknown, nullptr)));
+    }
+
+    virtual bool Process(OGRLayer &srcLayer, OGRLayer &dstLayer) = 0;
+
+    bool AddProcessedLayer(OGRLayer &srcLayer)
+    {
+        OGRMemLayer *poDstLayer = cpl::down_cast<OGRMemLayer *>(
path: apps/gdalalg_vector_pipeline.h
user: login: dbaston
body: Done in 20d580e62ed3d90a3655e305f13a8c8d53f92060
created_at: 2025-05-18T13:31:25Z
author_association: MEMBER
diff_hunk: @@ -135,6 +135,9 @@ class CPL_DLL MEMDataset CPL_NON_FINAL : public GDALDataset
 
     OGRLayer *GetLayer(int) override;
 
+    OGRLayer *CreateLayer(const OGRFeatureDefn &oDefn,
path: frmts/mem/memdataset.h
user: login: dbaston
body: Good idea, thanks
created_at: 2025-05-18T13:31:42Z
author_association: MEMBER

PR comments:
user: login: rouault
created_at: 2025-05-13T15:18:22Z
body: > his is much simpler, except that I didn't see a good way to create output layers with the same fields as the inputs. Is there something like `GDALDataset::CopyLayer` that copies only the layer definition, but not the contents?

we don't have that, but you can do something like:
```
for( int i = 0; i < poSrcLayerDefn->GetFieldCount(); ++i )
    m_poLayerDefn->AddFieldDefn(poSrcLayerDefn->GetFieldDefn(i);
m_poLayerDefn->SetGeomType(wkbNone);
for( int i = 0; i < poSrcLayerDefn->GetGeomFieldCount(); ++i )
    m_poLayerDefn->AddGeomFieldDefn(poSrcLayerDefn->GetGeomFieldDefn(i);
```
author_association: MEMBER
user: login: rouault
created_at: 2025-05-14T15:42:53Z
body: Memory leak reported by ASAN: https://github.com/dbaston/gdal/actions/runs/15022562126/job/42215139934
```
2025-05-14T14:55:42.3812332Z Direct leak of 176 byte(s) in 2 object(s) allocated from:
2025-05-14T14:55:42.3820167Z     #0 0x7feefd9260c1 in operator new(unsigned long) (/usr/lib/llvm-18/lib/clang/18/lib/linux/libclang_rt.asan-x86_64.so+0x1080c1) (BuildId: bf9e3747052c46cd49f7974555b1700dce30b8c0)
2025-05-14T14:55:42.3821320Z     #1 0x7feedb846d01 in GEOSCoverageSimplifyVW_r (/lib/x86_64-linux-gnu/libgeos_c.so.1+0x30d01) (BuildId: 1f19a68dc21881d713aaefcc10347bebb83f1fdf)
2025-05-14T14:55:42.3822432Z     #2 0x7feeefdb81b1 in GDALVectorGeomSimplifyCoverageAlgorithmLayer::Materialize() /home/runner/work/gdal/gdal/apps/gdalalg_vector_geom_simplify_coverage.cpp:220:37
2025-05-14T14:55:42.3823640Z     #3 0x7feeefdb37ce in GDALVectorGeomSimplifyCoverageAlgorithmLayer::GetNextFeature() /home/runner/work/gdal/gdal/apps/gdalalg_vector_geom_simplify_coverage.cpp:129:18
2025-05-14T14:55:42.3825025Z     #4 0x7feeeeb05565 in OGRLayer::FeatureIterator::FeatureIterator(OGRLayer*, bool) /home/runner/work/gdal/gdal/ogr/ogrsf_frmts/generic/ogrlayer.cpp:6053:41
2025-05-14T14:55:42.3825907Z     #5 0x7feeeeac63c0 in OGRLayer::begin() /home/runner/work/gdal/gdal/ogr/ogrsf_frmts/generic/ogrlayer.cpp:6106:12
2025-05-14T14:55:42.3826875Z     #6 0x7feeeeac5e62 in OGRLayer::GetFeatureCount(int) /home/runner/work/gdal/gdal/ogr/ogrsf_frmts/generic/ogrlayer.cpp:165:27
2025-05-14T14:55:42.3827667Z     #7 0x7feeeeac6e77 in OGR_L_GetFeatureCount /home/runner/work/gdal/gdal/ogr/ogrsf_frmts/generic/ogrlayer.cpp:189:42
2025-05-14T14:55:42.3828524Z     #8 0x7feed5eed41a in OGRLayerShadow_GetFeatureCount /home/runner/work/gdal/gdal/build-asan/swig/python/extensions/ogr_wrap.cpp:4674:33
2025-05-14T14:55:42.3829438Z     #9 0x7feed5eed41a in _wrap_Layer_GetFeatureCount /home/runner/work/gdal/gdal/build-asan/swig/python/extensions/ogr_wrap.cpp:10734:46
2025-05-14T14:55:42.3829923Z 
```
author_association: MEMBER
user: login: rouault
created_at: 2025-05-17T13:39:28Z
body: > we don't have that, but you can do something like:
> 
> ```
> for( int i = 0; i < poSrcLayerDefn->GetFieldCount(); ++i )
>     m_poLayerDefn->AddFieldDefn(poSrcLayerDefn->GetFieldDefn(i);
> m_poLayerDefn->SetGeomType(wkbNone);
> for( int i = 0; i < poSrcLayerDefn->GetGeomFieldCount(); ++i )
>     m_poLayerDefn->AddGeomFieldDefn(poSrcLayerDefn->GetGeomFieldDefn(i);
> ```

I was totally forgetting we have OGRFeatureDefn::Clone(), which is actually used in gdalalg_vector_geom_set_type.cpp
author_association: MEMBER
user: login: dbaston
created_at: 2025-05-17T20:50:31Z
body: > I was totally forgetting we have OGRFeatureDefn::Clone(), which is actually used in gdalalg_vector_geom_set_type.cpp

Unless I'm missing something, it only helps if I'm creating my own `OGRLayer` implementation...I can't use a FeatureDefn to create a Layer.
author_association: MEMBER
user: login: rouault
created_at: 2025-05-17T20:59:16Z
body: > Unless I'm missing something, it only helps if I'm creating my own `OGRLayer` implementation...I can't use a FeatureDefn to create a Layer.

you could add a OGRMemLayer::OGRMemLayer(const OGRFeatureDefn& ) constructor in frmts/mem/ogrmemlayer.cpp
author_association: MEMBER
user: login: dbaston
created_at: 2025-05-18T01:39:48Z
body: `test_gdalalg_vector_geom_simplify_coverage_test_ogrsf` Failing with 

```
E           ERROR: Driver GDALG does not declare GDAL_DCAP_UPDATE
```
author_association: MEMBER
user: login: rouault
created_at: 2025-05-18T01:48:43Z
body: > `E ERROR: Driver GDALG does not declare GDAL_DCAP_UPDATE`

cf https://github.com/OSGeo/gdal/blob/master/apps/test_ogrsf.cpp#L4434  . You should call OGRMemLayer::SetUpdatable(false) (after you've filled it)
author_association: MEMBER
user: login: rouault
created_at: 2025-05-18T01:49:38Z
body: you should rebase on top of latest master to make CI happy to take into account the change of signature of RunStep()
author_association: MEMBER
user: login: coveralls
created_at: 2025-05-18T02:59:40Z
body: 
[![Coverage Status](https://coveralls.io/builds/73722598/badge)](https://coveralls.io/builds/73722598)

coverage: 70.918% (-0.01%) from 70.931%
when pulling **d40f27e3b469006e803993078daee21c57959181 on dbaston:gdalalg-vector-geom-simplify-coverage**
into **2cf7f89c26d45913fe8a55e1f5c80610ac21b842 on OSGeo:master**.

author_association: COLLABORATOR