[geos-commits] [SCM] GEOS branch main updated. 278f015fbd3e9d48af5670d86e1681035a0f32ce

git at osgeo.org git at osgeo.org
Wed Jun 24 06:37:32 PDT 2026


This is an automated email from the git hooks/post-receive script. It was
generated because a ref change was pushed to the repository containing
the project "GEOS".

The branch, main has been updated
       via  278f015fbd3e9d48af5670d86e1681035a0f32ce (commit)
       via  b53ce984bb8ab784567a030fd3a852bb0abc76f0 (commit)
       via  6acb08f63e374978ac1c8d53388c6d20047c13bd (commit)
       via  5154841c7897347f5e91f16ba9dc38d1157ae1b6 (commit)
       via  975874cd09a909b9b8bf75006955ba7f965c991f (commit)
       via  f5fde7302d23dc01e061155ceec37df9e21a9f32 (commit)
       via  e64dcfe18725e08e6cad0a9909679c5703a9f25a (commit)
       via  03923866d44239f65a1261070236d04ef9c4cf6a (commit)
       via  ffbc5581587a4b2ad107b7aaa44a008fa068d3ca (commit)
       via  8626c42e21a3f0acfd2d8cc01c0c1f2a2c9bba70 (commit)
       via  61dbd1a974403a09e33e6b10402cb9d5a3bf4e05 (commit)
       via  1d65863b4972dd8050f2fa2aba73a5618775c305 (commit)
       via  8c7726f8a3c336e0b7db7b000aa8450266222f53 (commit)
       via  7307e49f43d11c456e14d3b0c814316bd5417de3 (commit)
       via  28712d504783adf4da7b34b125f245e0d1a99915 (commit)
       via  99a971a9c14f17964fac94956c88ba8c31053d37 (commit)
       via  6a74dc8d8ef6126d05bb3430c9644918e54484cd (commit)
       via  38ccee23700c1d572af227dc5f399f1cc45440fd (commit)
       via  b25a2ed25c74d8a3dc06c728f619ed524e611bed (commit)
      from  72a954f63aab8eaec71d6570d900b3450e0ea946 (commit)

Those revisions listed above that are new to this repository have
not appeared on any other notification email; so we list those
revisions in full, below.

- Log -----------------------------------------------------------------
commit 278f015fbd3e9d48af5670d86e1681035a0f32ce
Author: Daniel Baston <dbaston at gmail.com>
Date:   Tue Jun 23 14:56:23 2026 -0400

    CurveToLine: Allow specifying both max angle and max deviation

diff --git a/capi/geos_c.cpp b/capi/geos_c.cpp
index 982bb0a51..ec799f7d3 100644
--- a/capi/geos_c.cpp
+++ b/capi/geos_c.cpp
@@ -228,9 +228,15 @@ extern "C" {
     }
 
     int
-    GEOSCurveToLineParams_setTolerance(GEOSCurveToLineParams* params, enum GEOSCurveToLineTolerance toleranceType, double toleranceValue)
+    GEOSCurveToLineParams_setMaxStepDegrees(GEOSCurveToLineParams* params, double tolerance)
     {
-        return GEOSCurveToLineParams_setTolerance_r(handle, params, toleranceType, toleranceValue);
+        return GEOSCurveToLineParams_setMaxStepDegrees_r(handle, params, tolerance);
+    }
+
+    int
+    GEOSCurveToLineParams_setMaxDeviation(GEOSCurveToLineParams* params, double tolerance)
+    {
+        return GEOSCurveToLineParams_setMaxDeviation_r(handle, params, tolerance);
     }
 
     void
diff --git a/capi/geos_c.h.in b/capi/geos_c.h.in
index 4d7bc49b8..c6e853dea 100644
--- a/capi/geos_c.h.in
+++ b/capi/geos_c.h.in
@@ -1864,28 +1864,23 @@ extern double GEOS_DLL GEOSGeom_getPrecision_r(
     GEOSContextHandle_t handle,
     const GEOSGeometry *g);
 
-/**
-* Curve-to-line tolerance types.
-*/
-enum GEOSCurveToLineTolerance {
-    /** Maximum angle between successive vertices of a linearized arc */
-    GEOS_CURVETOLINE_STEP_DEGREES = 0,
-    /** Maximum distance between an arc and its linearized approximation */
-    GEOS_CURVETOLINE_MAX_DEVIATION = 1
-};
-
 /** \see GEOSCurveToLine */
 extern GEOSGeometry GEOS_DLL *GEOSCurveToLine_r(
     GEOSContextHandle_t handle,
     const GEOSGeometry* g,
     const GEOSCurveToLineParams* params);
 
-/** \see GEOSCurveToLineParams_setTolerance */
-extern int GEOS_DLL GEOSCurveToLineParams_setTolerance_r(
+/** \see GEOSCurveToLineParams_setMaxStepDegrees */
+extern int GEOS_DLL GEOSCurveToLineParams_setMaxStepDegrees_r(
     GEOSContextHandle_t extHandle,
     GEOSCurveToLineParams* params,
-    enum GEOSCurveToLineTolerance toleranceType,
-    double toleranceValue);
+    double tolerance);
+
+/** \see GEOSCurveToLineParams_setMaxDeviation */
+extern int GEOS_DLL GEOSCurveToLineParams_setMaxDeviation_r(
+    GEOSContextHandle_t extHandle,
+    GEOSCurveToLineParams* params,
+    double tolerance);
 
 /** \see GEOSLineToCurve */
 extern GEOSGeometry GEOS_DLL *GEOSLineToCurve_r(
@@ -6017,21 +6012,29 @@ extern GEOSCurveToLineParams GEOS_DLL *GEOSCurveToLineParams_create();
 
 extern void GEOS_DLL GEOSCurveToLineParams_destroy(GEOSCurveToLineParams* params);
 
-/** Set the tolerance type and value to use when converting curves to lines.
-*
-*   Any existing tolerance type and value will be overwritten.
+/** Set the maximum step (in degrees) between subsequent points.
 *
 *   \param params parameters to use in the conversion.
-*   \param toleranceType \see GEOSCurveToLineTolerance
-*   \param toleranceValue the value to use with toleranceType
-*   \return 1 if the operation succeeded, 0 otherwise*
+*   \param tolerance the maximum step size
+*   \return 1 if the operation succeeded, 0 otherwise
 *
 *   \since 3.15
 */ 
-extern int GEOS_DLL GEOSCurveToLineParams_setTolerance(
+extern int GEOS_DLL GEOSCurveToLineParams_setMaxStepDegrees(
     GEOSCurveToLineParams* params,
-    enum GEOSCurveToLineTolerance toleranceType,
-    double toleranceValue);
+    double tolerance);
+
+/** Set the maximum deviation between an arc and its linear approximate.
+*
+*   \param params parameters to use in the conversion.
+*   \param tolerance the maximum deviation
+*   \return 1 if the operation succeeded, 0 otherwise
+*
+*   \since 3.15
+*/ 
+extern int GEOS_DLL GEOSCurveToLineParams_setMaxDeviation(
+    GEOSCurveToLineParams* params,
+    double tolerance);
 
 /** Replace line segments in the geometry with circular arcs, where possible.
 *
@@ -6071,7 +6074,7 @@ extern int GEOS_DLL GEOSLineToCurveParams_setRadiusTolerance(
 *   A point may be considered to continue a circular arc if the angle between
 *   the point and the final point in the arc is less than the specified
 *   tolerance. Because of round-off errors, this value should be slightly
-*   greater than the one used in \ref GEOSCurveToLineParams_setTolerance.
+*   greater than the one used in \ref GEOSCurveToLineParams_setMaxStepDegrees.
 * 
 *   \param params the parameters object
 *   \param tolerance the tolerance value
diff --git a/capi/geos_ts_c.cpp b/capi/geos_ts_c.cpp
index 7f29f41b6..c42bf851d 100644
--- a/capi/geos_ts_c.cpp
+++ b/capi/geos_ts_c.cpp
@@ -3818,11 +3818,21 @@ extern "C" {
     }
 
     int
-    GEOSCurveToLineParams_setTolerance_r(GEOSContextHandle_t extHandle, GEOSCurveToLineParams* params,
-                                         GEOSCurveToLineTolerance toleranceType, double toleranceValue)
+    GEOSCurveToLineParams_setMaxStepDegrees_r(GEOSContextHandle_t extHandle, GEOSCurveToLineParams* params,
+                                              double toleranceValue)
     {
         return execute(extHandle, 0, [&]() {
-            params->setTolerance(static_cast<GEOSCurveToLineParams::TOLERANCE_TYPE>(toleranceType), toleranceValue);
+            params->setMaxStepDegrees(toleranceValue);
+            return 1;
+        });
+    }
+
+    int
+    GEOSCurveToLineParams_setMaxDeviation_r(GEOSContextHandle_t extHandle, GEOSCurveToLineParams* params,
+                                            double toleranceValue)
+    {
+        return execute(extHandle, 0, [&]() {
+            params->setMaxDeviation(toleranceValue);
             return 1;
         });
     }
diff --git a/include/geos/algorithm/CurveToLineParams.h b/include/geos/algorithm/CurveToLineParams.h
index 7de4d974c..25cbbe0f6 100644
--- a/include/geos/algorithm/CurveToLineParams.h
+++ b/include/geos/algorithm/CurveToLineParams.h
@@ -22,63 +22,54 @@ namespace geos::algorithm {
 class GEOS_DLL CurveToLineParams {
 
 public:
-    enum class TOLERANCE_TYPE {
-        STEP_DEGREES,
-        MAX_DEVIATION,
-    };
 
-    CurveToLineParams() : CurveToLineParams(TOLERANCE_TYPE::STEP_DEGREES, 4.0) {}
+    CurveToLineParams() :
+      m_maxStepDegrees(4.0),
+      m_maxDeviation(DoubleInfinity) {}
 
-    CurveToLineParams(TOLERANCE_TYPE tolType, double tolValue) {
-        setTolerance(tolType, tolValue);
-    }
-
-    void setTolerance(TOLERANCE_TYPE tolType, double tolValue) {
-        if (tolType == TOLERANCE_TYPE::STEP_DEGREES) {
-            if (!(tolValue > 0)) {
-                throw util::IllegalArgumentException("Step size must be positive");
-            }
-        } else if (tolType == TOLERANCE_TYPE::MAX_DEVIATION) {
-            if (!(tolValue > 0)) {
-                throw util::IllegalArgumentException("Max deviation must be positive");
-            }
-        } else {
-            throw util::IllegalArgumentException("Invalid tolerance type");
+    CurveToLineParams& setMaxStepDegrees(double value) {
+        if (!(value > 0)) {
+            throw util::IllegalArgumentException("Step size must be positive");
         }
 
-        toleranceType = tolType;
-        toleranceValue = tolValue;
+        m_maxStepDegrees = value;
+
+        return *this;
+    }
+
+    CurveToLineParams& setMaxDeviation(double value) {
+        if (!(value > 0)) {
+            throw util::IllegalArgumentException("Max deviation must be positive");
+        }
+
+        m_maxDeviation = value;
+
+        return *this;
     }
 
     static CurveToLineParams maxDeviation(double dev) {
-        return CurveToLineParams(TOLERANCE_TYPE::MAX_DEVIATION, dev);
+        return CurveToLineParams()
+          .setMaxStepDegrees(DoubleInfinity)
+          .setMaxDeviation(dev);
     }
 
     static CurveToLineParams stepSizeDegrees(double stepSize) {
-        return CurveToLineParams(TOLERANCE_TYPE::STEP_DEGREES, stepSize);
+        return CurveToLineParams()
+          .setMaxStepDegrees(stepSize)
+          .setMaxDeviation(DoubleInfinity);
     }
 
     double getStepSizeDegrees(const geom::CircularArc& arc) const {
-        if (toleranceType == TOLERANCE_TYPE::STEP_DEGREES) {
-            return toleranceValue;
-        }
+        const double deviationRatio = 1 - m_maxDeviation / arc.getRadius();
+        const double maxStepFromDeviation = deviationRatio < 0 ? 180 : std::acos(deviationRatio) * 360 / MATH_PI;
 
-        if (toleranceType == TOLERANCE_TYPE::MAX_DEVIATION) {
-            const double ratio = 1 - toleranceValue / arc.getRadius();
-            if (ratio < 0) {
-                return 180;
-            }
-
-            return std::acos(ratio) * 360 / MATH_PI;
-        }
-
-        throw util::IllegalArgumentException("Invalid tolerance type");
+        return std::min(m_maxStepDegrees, maxStepFromDeviation);
     }
 
 private:
 
-    TOLERANCE_TYPE toleranceType;
-    double toleranceValue;
+    double m_maxStepDegrees;
+    double m_maxDeviation;
 
 };
 
diff --git a/tests/unit/capi/GEOSClipByRectTest.cpp b/tests/unit/capi/GEOSClipByRectTest.cpp
index 31eacd2f4..e362fd0a1 100644
--- a/tests/unit/capi/GEOSClipByRectTest.cpp
+++ b/tests/unit/capi/GEOSClipByRectTest.cpp
@@ -303,7 +303,7 @@ void object::test<17>()
     result_ = GEOSClipByRect_r(ctxt_, input_, 0, 0, 1, 1);
     ensure(result_ == nullptr);
 
-    GEOSCurveToLineParams_setTolerance_r(ctxt_, curveToLineParams_, GEOS_CURVETOLINE_STEP_DEGREES, 1);
+    GEOSCurveToLineParams_setMaxStepDegrees_r(ctxt_, curveToLineParams_, 1);
     GEOSContext_setCurveToLineParams_r(ctxt_, curveToLineParams_);
 
     // Input converted to line, output not converted to curve
diff --git a/tests/unit/capi/GEOSCurveToLineTest.cpp b/tests/unit/capi/GEOSCurveToLineTest.cpp
index 8c4a2e47c..5fa90ca00 100644
--- a/tests/unit/capi/GEOSCurveToLineTest.cpp
+++ b/tests/unit/capi/GEOSCurveToLineTest.cpp
@@ -64,7 +64,7 @@ void object::test<3>()
 
     input_ = fromWKT("CIRCULARSTRING (0 0, 100 100, 200 0)");
 
-    ensure(GEOSCurveToLineParams_setTolerance(params_, GEOS_CURVETOLINE_STEP_DEGREES, 30));
+    ensure(GEOSCurveToLineParams_setMaxStepDegrees(params_, 30));
     result_ = GEOSCurveToLine(input_, params_);
     expected_ = fromWKT("LINESTRING(0 0,13.3975 50,50 86.6025,100 100,150 86.6025,186.6025 50,200 0)");
 
@@ -78,7 +78,8 @@ void object::test<4>()
     set_test_name("CircularString, maximum deviation");
 
     input_ = fromWKT("CIRCULARSTRING (0 0, 100 100, 200 0)");
-    ensure(GEOSCurveToLineParams_setTolerance(params_, GEOS_CURVETOLINE_MAX_DEVIATION, 10));
+    ensure(GEOSCurveToLineParams_setMaxStepDegrees(params_, 90));
+    ensure(GEOSCurveToLineParams_setMaxDeviation(params_, 10));
     result_ = GEOSCurveToLine(input_, params_);
     expected_ = fromWKT("LINESTRING(0 0,30 70,100 100,170 70,200 0)");
 
@@ -92,8 +93,11 @@ void object::test<5>()
     set_test_name("invalid tolerance value");
 
     input_ = fromWKT("CIRCULARSTRING (0 0, 1 1, 2 0)");
-    ensure(!GEOSCurveToLineParams_setTolerance(params_, GEOS_CURVETOLINE_STEP_DEGREES, -1));
-    ensure(!GEOSCurveToLineParams_setTolerance(params_, GEOS_CURVETOLINE_STEP_DEGREES, std::numeric_limits<double>::quiet_NaN()));
+    ensure(!GEOSCurveToLineParams_setMaxStepDegrees(params_, -1));
+    ensure(!GEOSCurveToLineParams_setMaxStepDegrees(params_, std::numeric_limits<double>::quiet_NaN()));
+
+    ensure(!GEOSCurveToLineParams_setMaxDeviation(params_, -1));
+    ensure(!GEOSCurveToLineParams_setMaxDeviation(params_, std::numeric_limits<double>::quiet_NaN()));
 }
 
 } // namespace tut
\ No newline at end of file
diff --git a/tests/unit/capi/GEOSUnaryUnionTest.cpp b/tests/unit/capi/GEOSUnaryUnionTest.cpp
index c4abb8458..dea0c8941 100644
--- a/tests/unit/capi/GEOSUnaryUnionTest.cpp
+++ b/tests/unit/capi/GEOSUnaryUnionTest.cpp
@@ -231,7 +231,7 @@ void object::test<12>()
     result_ = GEOSUnaryUnion_r(ctxt_, input_);
     ensure(result_ == nullptr);
 
-    GEOSCurveToLineParams_setTolerance_r(ctxt_, curveToLineParams_, GEOS_CURVETOLINE_STEP_DEGREES, 1);
+    GEOSCurveToLineParams_setMaxStepDegrees_r(ctxt_, curveToLineParams_, 1);
     GEOSContext_setCurveToLineParams_r(ctxt_, curveToLineParams_);
 
     // Input converted to line, output not converted to curve

commit b53ce984bb8ab784567a030fd3a852bb0abc76f0
Author: Daniel Baston <dbaston at gmail.com>
Date:   Mon Jun 22 21:29:33 2026 -0400

    LineToCurve: Use same angle for difference tolerance as in absolute value tolerance

diff --git a/capi/geos_c.h.in b/capi/geos_c.h.in
index 7aca99468..4d7bc49b8 100644
--- a/capi/geos_c.h.in
+++ b/capi/geos_c.h.in
@@ -6068,9 +6068,10 @@ extern int GEOS_DLL GEOSLineToCurveParams_setRadiusTolerance(
 
 /** Set the maximum angle spacing used in line-to-curve conversion.
 *
-*   A vertex may be considered to continue a circular arc if the circular sector
-*   between the vertex and the final point in the arc is less than the specified tolerance. Because of round-off errors, this value should be slightly greater than the one used
-*   in \ref GEOSCurveToLineParams_setTolerance.
+*   A point may be considered to continue a circular arc if the angle between
+*   the point and the final point in the arc is less than the specified
+*   tolerance. Because of round-off errors, this value should be slightly
+*   greater than the one used in \ref GEOSCurveToLineParams_setTolerance.
 * 
 *   \param params the parameters object
 *   \param tolerance the tolerance value
@@ -6083,9 +6084,12 @@ extern int GEOS_DLL GEOSLineToCurveParams_setMaxStepDegrees(
 
 /** Set the maximum angle difference used in line-to-curve conversion
 *
-*   A vertex p[n] may be considered to continue a circular arc if the 
-*   difference between the angles p[n-2] / p[n-1] / p[n] is within the
-*   specified difference of the angle p[0] / p[1] / p[2].
+*   A point may be considered to continue a circular arc if the angle between
+*   the point and the final point in the arc is within the specified
+*   tolerance of the angle calculated when the final point in the arc was
+*   added. This tolerance can be used to ensure that the step spacing is
+*   regular, which is an indication of a line that was generated by 
+*   linearizing a curve.
 * 
 *   \param params the parameters object
 *   \param tolerance the tolerance value
diff --git a/capi/geos_ts_c.cpp b/capi/geos_ts_c.cpp
index 108f5d0d0..7f29f41b6 100644
--- a/capi/geos_ts_c.cpp
+++ b/capi/geos_ts_c.cpp
@@ -3840,7 +3840,7 @@ extern "C" {
     GEOSLineToCurveParams_setMaxAngleDifferenceDegrees_r(GEOSContextHandle_t extHandle, GEOSLineToCurveParams* params, double tolerance)
     {
         return execute(extHandle, 0, [&]() {
-            params->setMaxExteriorAngleDifferenceDegrees(tolerance);
+            params->setMaxAngleDifferenceDegrees(tolerance);
             return 1;
         });
     }
diff --git a/include/geos/algorithm/LineToCurveParams.h b/include/geos/algorithm/LineToCurveParams.h
index f76d3c6cb..1e2067929 100644
--- a/include/geos/algorithm/LineToCurveParams.h
+++ b/include/geos/algorithm/LineToCurveParams.h
@@ -40,9 +40,9 @@ public:
         return maxAngleRadians;
     }
 
-    // Get the maximum angle difference in radians (p[0], p[1], p[2]) between successive sections of the same arc.
-    double getMaxExteriorAngleDifferenceRadians() const {
-        return maxExteriorAngleDifferenceRadians;
+    // Get the maximum difference in sequentially calculated angles along the same arc.
+    double getMaxAngleDifferenceRadians() const {
+        return maxAngleDifferenceRadians;
     }
 
     void setRadiusTolerance(double tol) {
@@ -52,15 +52,15 @@ public:
         radiusTolerance = tol;
     }
 
-    void setMaxExteriorAngleDifferenceRadians(double tol) {
+    void setMaxAngleDifferenceRadians(double tol) {
         if (!(tol > 0)) {
             throw geos::util::IllegalArgumentException("Angle tolerance must be positive");
         }
-        maxExteriorAngleDifferenceRadians = tol;
+        maxAngleDifferenceRadians = tol;
     }
 
-    void setMaxExteriorAngleDifferenceDegrees(double tol) {
-        setMaxExteriorAngleDifferenceRadians(tol * MATH_PI / 180.0);
+    void setMaxAngleDifferenceDegrees(double tol) {
+        setMaxAngleDifferenceRadians(tol * MATH_PI / 180.0);
     }
 
     void setMaxStepDegrees(double tol) {
@@ -73,7 +73,7 @@ public:
 private:
     double radiusTolerance{1e-6};
     double maxAngleRadians{45.01 * MATH_PI / 180.0};
-    double maxExteriorAngleDifferenceRadians{0.01};
+    double maxAngleDifferenceRadians{0.01};
 };
 
 }
\ No newline at end of file
diff --git a/src/algorithm/LineToCurveConverter.cpp b/src/algorithm/LineToCurveConverter.cpp
index b50668ce2..072638bb2 100644
--- a/src/algorithm/LineToCurveConverter.cpp
+++ b/src/algorithm/LineToCurveConverter.cpp
@@ -102,12 +102,11 @@ LineToCurveConverter::compute(const LineString& ls, const LineToCurveParams& par
                 break;
             }
 
-            // Check that the angle p[stop-2] /_ p[stop-1] /_ p[stop] is consistent with that in the
-            // original section of the arc.
-            const double prevExtAngle = Angle::angleBetween(arc.p0(), arc.p1(), arc.p2());
-            const double currExtAngle = Angle::angleBetween(prev2, prev1, pt);
-
-            if (std::abs(currExtAngle - prevExtAngle) > params.getMaxExteriorAngleDifferenceRadians()) {
+            // Check that angle p[stop-1] /_ center /_ p[stop] is consistent with that in the
+            // original section of the arc. This is a different formulation than PostGIS, which
+            // performs the test on the exterior angle p[stop-2] /_ p[stop-1] /_ p[stop].
+            const double prevAngle = std::abs(Angle::angleBetweenOriented(prev2, arc.getCenter(), prev1));
+            if (std::abs(currAngle - prevAngle) > params.getMaxAngleDifferenceRadians()) {
                 break;
             }
 

commit 6acb08f63e374978ac1c8d53388c6d20047c13bd
Author: Daniel Baston <dbaston at gmail.com>
Date:   Fri Jun 19 15:58:34 2026 -0400

    LineToCurve: Move into LineToCurveConverter, remove duplicated code

diff --git a/include/geos/algorithm/CurveBuilder.h b/include/geos/algorithm/LineToCurveConverter.h
similarity index 71%
rename from include/geos/algorithm/CurveBuilder.h
rename to include/geos/algorithm/LineToCurveConverter.h
index 1739dc540..b14b3f98a 100644
--- a/include/geos/algorithm/CurveBuilder.h
+++ b/include/geos/algorithm/LineToCurveConverter.h
@@ -15,6 +15,7 @@
 #pragma once
 
 #include <geos/export.h>
+#include <geos/geom/util/CurveBuilder.h>
 
 #include <memory>
 
@@ -33,15 +34,18 @@ namespace geos::geom {
 
 namespace geos::algorithm {
 
-class GEOS_DLL CurveBuilder {
+class GEOS_DLL LineToCurveConverter {
 
 public:
 
+    /** Construct a curved geometry from the provided LineString. The provided LineToCurveParams will be used
+     *  to determine when successive vertices of the LineString can be interpreted as a circular arc.
+     */
     static std::unique_ptr<geom::Curve> getCurved(const geom::LineString& ls, const LineToCurveParams& params);
 
 private:
 
-    explicit CurveBuilder(const geom::GeometryFactory& factory);
+    explicit LineToCurveConverter(const geom::GeometryFactory& factory, bool outputZ, bool outputM);
 
     std::unique_ptr<geom::Curve> compute(const geom::LineString& ls, const LineToCurveParams& params);
 
@@ -49,6 +53,8 @@ private:
 
     void addLineCoords(const geom::CoordinateSequence& points, std::size_t from, std::size_t to);
 
+    /*
+
     void finishArc();
 
     void finishLine();
@@ -57,10 +63,12 @@ private:
     std::shared_ptr<geom::CoordinateSequence> arcCoords;
     std::vector<std::unique_ptr<geom::SimpleCurve>> curves;
     const geom::GeometryFactory& factory;
+    */
+    geom::util::CurveBuilder curveBuilder;
 
     /// Declared as non-copyable
-    CurveBuilder(const CurveBuilder& other);
-    CurveBuilder& operator=(const CurveBuilder& rhs);
+    LineToCurveConverter(const LineToCurveConverter& other);
+    LineToCurveConverter& operator=(const LineToCurveConverter& rhs);
 };
 
 }
\ No newline at end of file
diff --git a/include/geos/geom/util/CurveBuilder.h b/include/geos/geom/util/CurveBuilder.h
index b1dc0dcbb..27fccd2fa 100644
--- a/include/geos/geom/util/CurveBuilder.h
+++ b/include/geos/geom/util/CurveBuilder.h
@@ -40,6 +40,9 @@ public:
     // Add all coordinates in the provided sequence
     void add(const CoordinateSequence& seq, bool isCurved);
 
+    // Add coordinates between the specified indices (inclusive)
+    void add(const CoordinateSequence& seq, std::size_t from, std::size_t to, bool isCurved);
+
     // Close the ring, if necessary, using a linear segment
     void closeRing();
 
diff --git a/src/algorithm/CurveBuilder.cpp b/src/algorithm/LineToCurveConverter.cpp
similarity index 82%
rename from src/algorithm/CurveBuilder.cpp
rename to src/algorithm/LineToCurveConverter.cpp
index ef3215fc1..b50668ce2 100644
--- a/src/algorithm/CurveBuilder.cpp
+++ b/src/algorithm/LineToCurveConverter.cpp
@@ -13,7 +13,7 @@
  **********************************************************************/
 
 #include <geos/algorithm/Angle.h>
-#include <geos/algorithm/CurveBuilder.h>
+#include <geos/algorithm/LineToCurveConverter.h>
 
 #include <geos/geom/CircularArc.h>
 #include <geos/geom/CircularString.h>
@@ -32,17 +32,21 @@ using geom::CoordinateXYZM;
 using geom::GeometryFactory;
 using geom::LineString;
 
-CurveBuilder::CurveBuilder(const GeometryFactory &p_factory)
-    : factory(p_factory) {}
+LineToCurveConverter::LineToCurveConverter(const GeometryFactory &factory, bool outputZ, bool outputM)
+    : curveBuilder(factory, outputZ, outputM)
+{
+    // CurvePolygon uses a closed LineString rather than LinearRing
+    curveBuilder.setOutputLinearRing(false);
+}
 
 std::unique_ptr<geom::Curve>
-CurveBuilder::getCurved(const LineString &ls, const LineToCurveParams& params) {
-    CurveBuilder cb(*ls.getFactory());
+LineToCurveConverter::getCurved(const LineString &ls, const LineToCurveParams& params) {
+    LineToCurveConverter cb(*ls.getFactory(), ls.hasZ(), ls.hasM());
     return cb.compute(ls, params);
 }
 
 std::unique_ptr<geom::Curve>
-CurveBuilder::compute(const LineString& ls, const LineToCurveParams& params) {
+LineToCurveConverter::compute(const LineString& ls, const LineToCurveParams& params) {
     if (ls.isEmpty()) {
         return ls.clone();
     }
@@ -121,44 +125,13 @@ CurveBuilder::compute(const LineString& ls, const LineToCurveParams& params) {
 
     }
 
-    finishArc();
-    finishLine();
-
-    if (curves.size() == 1) {
-        return std::move(curves.front());
-    }
-
-    return factory.createCompoundCurve(std::move(curves));
+    return curveBuilder.getGeometry();
 }
 
 void
-CurveBuilder::addLineCoords(const CoordinateSequence& points, std::size_t from, std::size_t to)
+LineToCurveConverter::addLineCoords(const CoordinateSequence& points, std::size_t from, std::size_t to)
 {
-    finishArc();
-
-    if (lineCoords) {
-        lineCoords->add(points, from + 1, to);
-    } else {
-        lineCoords = std::make_shared<CoordinateSequence>(0, points.hasZ(), points.hasM());
-        lineCoords->add(points, from, to);
-    }
-}
-
-void
-CurveBuilder::finishArc()
-{
-    if (arcCoords) {
-        curves.push_back(factory.createCircularString(arcCoords));
-        arcCoords.reset();
-    }
-}
-
-void
-CurveBuilder::finishLine() {
-    if (lineCoords) {
-        curves.push_back(factory.createLineString(lineCoords));
-        lineCoords.reset();
-    }
+    curveBuilder.add(points, from, to, false);
 }
 
 /// Interpolates Z and M values from the midpoint of the arc, by:
@@ -219,12 +192,12 @@ getOrInterpolateMidPointZM(CoordinateXYZM& p1, const CircularArc& arc, size_t st
 }
 
 void
-CurveBuilder::addArc(const CircularArc& arc, std::size_t stop) {
-    finishLine();
-
+LineToCurveConverter::addArc(const CircularArc& arc, std::size_t stop) {
     const CoordinateSequence& points = *arc.getCoordinateSequence();
     std::size_t start = arc.getCoordinatePosition();
 
+    CoordinateSequence& dstCoords = curveBuilder.getSeq(true);
+
     double xSum = 0.0;
     double ySum = 0.0;
     std::size_t nArcApproximations = 0;
@@ -255,12 +228,7 @@ CurveBuilder::addArc(const CircularArc& arc, std::size_t stop) {
         CoordinateXYZM p01(CircularArcs::getMidpoint(p0, p1, averageCenter, averageRadius, isCCW));
         CoordinateXYZM p12(CircularArcs::getMidpoint(p1, p2, averageCenter, averageRadius, isCCW));
 
-        if (!arcCoords) {
-            // should always be the case
-            arcCoords = std::make_shared<CoordinateSequence>(0,  points.hasZ(), points.hasM());
-            arcCoords->reserve(5);
-            arcCoords->add(p0);
-        }
+        dstCoords.add(p0);
 
         if (points.hasZ() || points.hasM()) {
             auto nPoints = stop - start + 1;
@@ -285,10 +253,10 @@ CurveBuilder::addArc(const CircularArc& arc, std::size_t stop) {
             }
         }
 
-        arcCoords->add(p01);
-        arcCoords->add(p1);
-        arcCoords->add(p12);
-        arcCoords->add(p2);
+        dstCoords.add(p01);
+        dstCoords.add(p1);
+        dstCoords.add(p12);
+        dstCoords.add(p2);
     } else {
         CoordinateXYZM p1(CircularArcs::getMidpoint(p0, p2, averageCenter, averageRadius, arc.isCCW()));
 
@@ -296,14 +264,9 @@ CurveBuilder::addArc(const CircularArc& arc, std::size_t stop) {
             getOrInterpolateMidPointZM(p1, arc, stop, averageCenter);
         }
 
-        if (!arcCoords) {
-            arcCoords = std::make_shared<CoordinateSequence>(0,  points.hasZ(), points.hasM());
-            arcCoords->reserve(3);
-            arcCoords->add(p0);
-        }
-
-        arcCoords->add(p1);
-        arcCoords->add(p2);
+        dstCoords.add(p0, false);
+        dstCoords.add(p1);
+        dstCoords.add(p2);
     }
 }
 
diff --git a/src/geom/LineString.cpp b/src/geom/LineString.cpp
index ee04e0f3b..3e0123926 100644
--- a/src/geom/LineString.cpp
+++ b/src/geom/LineString.cpp
@@ -20,7 +20,7 @@
 
 #include <geos/util/IllegalArgumentException.h>
 #include <geos/algorithm/CircularArcs.h>
-#include <geos/algorithm/CurveBuilder.h>
+#include <geos/algorithm/LineToCurveConverter.h>
 #include <geos/algorithm/Length.h>
 #include <geos/algorithm/Orientation.h>
 #include <geos/geom/CircularArc.h>
@@ -39,6 +39,7 @@
 #include <geos/geom/MultiPoint.h> // for getBoundary
 #include <geos/geom/Envelope.h>
 #include <geos/operation/BoundaryOp.h>
+#include <geos/util/IllegalArgumentException.h>
 #include <geos/util.h>
 
 #include <algorithm>
@@ -102,7 +103,7 @@ LineString::validateConstruction()
     }
 
     if(points->size() == 1) {
-        throw util::IllegalArgumentException("point array must contain 0 or >1 elements\n");
+        throw geos::util::IllegalArgumentException("point array must contain 0 or >1 elements\n");
     }
 }
 
@@ -115,7 +116,7 @@ LineString::getCurvedImpl(const algorithm::LineToCurveParams& params) const
 std::unique_ptr<Curve>
 LineString::getCurved(const algorithm::LineToCurveParams& params) const
 {
-    return CurveBuilder::getCurved(*this, params);
+    return LineToCurveConverter::getCurved(*this, params);
 }
 
 std::string
diff --git a/src/geom/util/CurveBuilder.cpp b/src/geom/util/CurveBuilder.cpp
index 300c4ad21..28859a7cc 100644
--- a/src/geom/util/CurveBuilder.cpp
+++ b/src/geom/util/CurveBuilder.cpp
@@ -50,6 +50,12 @@ CurveBuilder::add(const CoordinateSequence& coords, bool isCurved)
     getSeq(isCurved).add(coords, false);
 }
 
+void
+CurveBuilder::add(const CoordinateSequence& coords, std::size_t from, std::size_t to, bool isCurved)
+{
+    getSeq(isCurved).add(coords, from, to, false);
+}
+
 void
 CurveBuilder::closeRing()
 {
diff --git a/tests/unit/algorithm/CurveBuilderTest.cpp b/tests/unit/algorithm/LineToCurveConverterTest.cpp
similarity index 97%
rename from tests/unit/algorithm/CurveBuilderTest.cpp
rename to tests/unit/algorithm/LineToCurveConverterTest.cpp
index 734b52a03..c848f42b5 100644
--- a/tests/unit/algorithm/CurveBuilderTest.cpp
+++ b/tests/unit/algorithm/LineToCurveConverterTest.cpp
@@ -11,7 +11,7 @@ using geos::algorithm::LineToCurveParams;
 
 namespace tut {
 
-struct test_algorithm_curvebuilder_data {
+struct test_algorithm_linetocurveconverter_data {
     geos::io::WKTReader reader_;
 
     void checkRoundTrip(const std::string& wkt_in, const std::string& wkt_expected, double stepSizeDegrees) const {
@@ -61,10 +61,10 @@ struct test_algorithm_curvebuilder_data {
     }
 };
 
-typedef test_group<test_algorithm_curvebuilder_data> group;
+typedef test_group<test_algorithm_linetocurveconverter_data> group;
 typedef group::object object;
 
-group test_algorithm_curvebuilder_group("geos::algorithm::CurveBuilder");
+group test_algorithm_linetocurveconverter_group("geos::algorithm::LineToCurveConverter");
 
 template<>
 template<>

commit 5154841c7897347f5e91f16ba9dc38d1157ae1b6
Author: Daniel Baston <dbaston at gmail.com>
Date:   Fri Jun 19 15:30:00 2026 -0400

    LineToCurveParams: Add some docstrings

diff --git a/include/geos/algorithm/LineToCurveParams.h b/include/geos/algorithm/LineToCurveParams.h
index 1cd88e374..f76d3c6cb 100644
--- a/include/geos/algorithm/LineToCurveParams.h
+++ b/include/geos/algorithm/LineToCurveParams.h
@@ -30,6 +30,21 @@ public:
         return radiusTolerance;
     }
 
+    // Get the maximum angle in degrees between successive vertices, measured from the circle center.
+    double getMaxAngleDegrees() const {
+        return maxAngleRadians * 180 / MATH_PI;
+    }
+
+    // Get the maximum angle in radians between two successive vertices, measured from the circle center.
+    double getMaxAngleRadians() const {
+        return maxAngleRadians;
+    }
+
+    // Get the maximum angle difference in radians (p[0], p[1], p[2]) between successive sections of the same arc.
+    double getMaxExteriorAngleDifferenceRadians() const {
+        return maxExteriorAngleDifferenceRadians;
+    }
+
     void setRadiusTolerance(double tol) {
         if (!(tol > 0)) {
             throw geos::util::IllegalArgumentException("Radius tolerance must be positive");
@@ -37,10 +52,6 @@ public:
         radiusTolerance = tol;
     }
 
-    double getMaxExteriorAngleDifferenceRadians() const {
-        return maxExteriorAngleDifferenceRadians;
-    }
-
     void setMaxExteriorAngleDifferenceRadians(double tol) {
         if (!(tol > 0)) {
             throw geos::util::IllegalArgumentException("Angle tolerance must be positive");
@@ -52,15 +63,6 @@ public:
         setMaxExteriorAngleDifferenceRadians(tol * MATH_PI / 180.0);
     }
 
-    // Get the maximum angle step.
-    double getMaxAngleDegrees() const {
-        return maxAngleRadians * 180 / MATH_PI;
-    }
-
-    double getMaxAngleRadians() const {
-        return maxAngleRadians;
-    }
-
     void setMaxStepDegrees(double tol) {
         if (!(tol > 0)) {
             throw util::IllegalArgumentException("Angle step tolerance must be positive");

commit 975874cd09a909b9b8bf75006955ba7f965c991f
Author: Daniel Baston <dbaston at gmail.com>
Date:   Wed Jun 17 20:26:45 2026 -0400

    GEOSCurveToLineTest: remove test for invalid tolerance type

diff --git a/tests/unit/capi/GEOSCurveToLineTest.cpp b/tests/unit/capi/GEOSCurveToLineTest.cpp
index e251eb2c7..8c4a2e47c 100644
--- a/tests/unit/capi/GEOSCurveToLineTest.cpp
+++ b/tests/unit/capi/GEOSCurveToLineTest.cpp
@@ -85,28 +85,9 @@ void object::test<4>()
     ensure_geometry_equals_exact(result_, expected_, 1.4);
 }
 
-#if defined(__GNUC__) || defined(__clang__)
-#pragma GCC diagnostic push
-#pragma GCC diagnostic ignored "-Wconversion"
-#endif
-
 template<>
 template<>
 void object::test<5>()
-{
-    set_test_name("invalid tolerance type");
-
-    input_ = fromWKT("CIRCULARSTRING (0 0, 1 1, 2 0)");
-    ensure(!GEOSCurveToLineParams_setTolerance(params_, static_cast<GEOSCurveToLineTolerance>(-3), 10));
-}
-
-#if defined(__GNUC__) || defined(__clang__)
-#pragma GCC diagnostic pop
-#endif
-
-template<>
-template<>
-void object::test<6>()
 {
     set_test_name("invalid tolerance value");
 

commit f5fde7302d23dc01e061155ceec37df9e21a9f32
Author: Daniel Baston <dbaston at gmail.com>
Date:   Wed Jun 17 17:13:09 2026 -0400

    GEOSCurveToLine: address comments from @rouault

diff --git a/capi/geos_c.cpp b/capi/geos_c.cpp
index a664d9e21..982bb0a51 100644
--- a/capi/geos_c.cpp
+++ b/capi/geos_c.cpp
@@ -228,7 +228,7 @@ extern "C" {
     }
 
     int
-    GEOSCurveToLineParams_setTolerance(GEOSCurveToLineParams* params, int toleranceType, double toleranceValue)
+    GEOSCurveToLineParams_setTolerance(GEOSCurveToLineParams* params, enum GEOSCurveToLineTolerance toleranceType, double toleranceValue)
     {
         return GEOSCurveToLineParams_setTolerance_r(handle, params, toleranceType, toleranceValue);
     }
diff --git a/capi/geos_c.h.in b/capi/geos_c.h.in
index b88f41e90..7aca99468 100644
--- a/capi/geos_c.h.in
+++ b/capi/geos_c.h.in
@@ -1884,7 +1884,7 @@ extern GEOSGeometry GEOS_DLL *GEOSCurveToLine_r(
 extern int GEOS_DLL GEOSCurveToLineParams_setTolerance_r(
     GEOSContextHandle_t extHandle,
     GEOSCurveToLineParams* params,
-    int toleranceType,
+    enum GEOSCurveToLineTolerance toleranceType,
     double toleranceValue);
 
 /** \see GEOSLineToCurve */
@@ -6019,6 +6019,8 @@ extern void GEOS_DLL GEOSCurveToLineParams_destroy(GEOSCurveToLineParams* params
 
 /** Set the tolerance type and value to use when converting curves to lines.
 *
+*   Any existing tolerance type and value will be overwritten.
+*
 *   \param params parameters to use in the conversion.
 *   \param toleranceType \see GEOSCurveToLineTolerance
 *   \param toleranceValue the value to use with toleranceType
@@ -6028,7 +6030,7 @@ extern void GEOS_DLL GEOSCurveToLineParams_destroy(GEOSCurveToLineParams* params
 */ 
 extern int GEOS_DLL GEOSCurveToLineParams_setTolerance(
     GEOSCurveToLineParams* params,
-    int toleranceType,
+    enum GEOSCurveToLineTolerance toleranceType,
     double toleranceValue);
 
 /** Replace line segments in the geometry with circular arcs, where possible.
diff --git a/capi/geos_ts_c.cpp b/capi/geos_ts_c.cpp
index 63fbf96fe..108f5d0d0 100644
--- a/capi/geos_ts_c.cpp
+++ b/capi/geos_ts_c.cpp
@@ -3819,7 +3819,7 @@ extern "C" {
 
     int
     GEOSCurveToLineParams_setTolerance_r(GEOSContextHandle_t extHandle, GEOSCurveToLineParams* params,
-                                         int toleranceType, double toleranceValue)
+                                         GEOSCurveToLineTolerance toleranceType, double toleranceValue)
     {
         return execute(extHandle, 0, [&]() {
             params->setTolerance(static_cast<GEOSCurveToLineParams::TOLERANCE_TYPE>(toleranceType), toleranceValue);
diff --git a/tests/unit/capi/GEOSCurveToLineTest.cpp b/tests/unit/capi/GEOSCurveToLineTest.cpp
index a708d3526..e251eb2c7 100644
--- a/tests/unit/capi/GEOSCurveToLineTest.cpp
+++ b/tests/unit/capi/GEOSCurveToLineTest.cpp
@@ -85,6 +85,11 @@ void object::test<4>()
     ensure_geometry_equals_exact(result_, expected_, 1.4);
 }
 
+#if defined(__GNUC__) || defined(__clang__)
+#pragma GCC diagnostic push
+#pragma GCC diagnostic ignored "-Wconversion"
+#endif
+
 template<>
 template<>
 void object::test<5>()
@@ -92,9 +97,13 @@ void object::test<5>()
     set_test_name("invalid tolerance type");
 
     input_ = fromWKT("CIRCULARSTRING (0 0, 1 1, 2 0)");
-    ensure(!GEOSCurveToLineParams_setTolerance(params_, -3, 10));
+    ensure(!GEOSCurveToLineParams_setTolerance(params_, static_cast<GEOSCurveToLineTolerance>(-3), 10));
 }
 
+#if defined(__GNUC__) || defined(__clang__)
+#pragma GCC diagnostic pop
+#endif
+
 template<>
 template<>
 void object::test<6>()

commit e64dcfe18725e08e6cad0a9909679c5703a9f25a
Author: Daniel Baston <dbaston at gmail.com>
Date:   Wed Jun 17 17:33:00 2026 -0400

    CurveToLineParams: avoid returning stepDegress=NaN

diff --git a/include/geos/algorithm/CurveToLineParams.h b/include/geos/algorithm/CurveToLineParams.h
index 9e1ba5deb..7de4d974c 100644
--- a/include/geos/algorithm/CurveToLineParams.h
+++ b/include/geos/algorithm/CurveToLineParams.h
@@ -64,7 +64,12 @@ public:
         }
 
         if (toleranceType == TOLERANCE_TYPE::MAX_DEVIATION) {
-            return std::acos(1 - toleranceValue / arc.getRadius()) * 360 / MATH_PI;
+            const double ratio = 1 - toleranceValue / arc.getRadius();
+            if (ratio < 0) {
+                return 180;
+            }
+
+            return std::acos(ratio) * 360 / MATH_PI;
         }
 
         throw util::IllegalArgumentException("Invalid tolerance type");

commit 03923866d44239f65a1261070236d04ef9c4cf6a
Author: Daniel Baston <dbaston at gmail.com>
Date:   Wed Jun 17 16:06:00 2026 -0400

    Header include fixes

diff --git a/include/geos/algorithm/LineToCurveParams.h b/include/geos/algorithm/LineToCurveParams.h
index 9690df289..1cd88e374 100644
--- a/include/geos/algorithm/LineToCurveParams.h
+++ b/include/geos/algorithm/LineToCurveParams.h
@@ -15,6 +15,8 @@
 #pragma once
 
 #include <geos/export.h>
+#include <geos/constants.h>
+#include <geos/util/IllegalArgumentException.h>
 
 namespace geos::algorithm {
 
@@ -30,7 +32,7 @@ public:
 
     void setRadiusTolerance(double tol) {
         if (!(tol > 0)) {
-            throw util::IllegalArgumentException("Radius tolerance must be positive");
+            throw geos::util::IllegalArgumentException("Radius tolerance must be positive");
         }
         radiusTolerance = tol;
     }
@@ -41,7 +43,7 @@ public:
 
     void setMaxExteriorAngleDifferenceRadians(double tol) {
         if (!(tol > 0)) {
-            throw util::IllegalArgumentException("Angle tolerance must be positive");
+            throw geos::util::IllegalArgumentException("Angle tolerance must be positive");
         }
         maxExteriorAngleDifferenceRadians = tol;
     }
diff --git a/src/geom/CompoundCurve.cpp b/src/geom/CompoundCurve.cpp
index 07ca9a57e..6eef96d8d 100644
--- a/src/geom/CompoundCurve.cpp
+++ b/src/geom/CompoundCurve.cpp
@@ -265,9 +265,9 @@ CompoundCurve::getLinearizedImpl(const algorithm::CurveToLineParams& params) con
         }
 
         if (curve->isCurved()) {
-            for (std::size_t i = 0; i < curveSeq->size() - 2; i += 2)
-            {
-                CircularArc arc(*curveSeq, i);
+            const CircularString* cs = detail::down_cast<const CircularString*>(curve.get());
+
+            for (const auto& arc : cs->getArcs()) {
                 arc.addLinearizedPoints(*seq, params);
             }
         } else {
diff --git a/src/geom/CurvePolygon.cpp b/src/geom/CurvePolygon.cpp
index 4986f5622..86f9dd6b2 100644
--- a/src/geom/CurvePolygon.cpp
+++ b/src/geom/CurvePolygon.cpp
@@ -13,6 +13,7 @@
  **********************************************************************/
 
 #include <geos/algorithm/Area.h>
+#include <geos/algorithm/CurveToLineParams.h>
 #include <geos/geom/Curve.h>
 #include <geos/geom/CurvePolygon.h>
 #include <geos/geom/CoordinateSequence.h>
@@ -22,8 +23,6 @@
 #include <geos/geom/SimpleCurve.h>
 #include <geos/util/UnsupportedOperationException.h>
 
-#include "geos/algorithm/CurveToLineParams.h"
-
 namespace geos {
 namespace geom {
 
diff --git a/src/geom/MultiLineString.cpp b/src/geom/MultiLineString.cpp
index 91d4f1860..c58615076 100644
--- a/src/geom/MultiLineString.cpp
+++ b/src/geom/MultiLineString.cpp
@@ -18,6 +18,7 @@
  *
  **********************************************************************/
 
+#include <geos/algorithm/LineToCurveParams.h>
 #include <geos/geomgraph/GeometryGraph.h>
 #include <geos/geom/GeometryFactory.h>
 #include <geos/geom/MultiCurve.h>
@@ -27,7 +28,6 @@
 
 #include <vector>
 
-#include "geos/algorithm/LineToCurveParams.h"
 
 using geos::geom::GeometryFactory;
 using geos::geom::MultiLineString;
diff --git a/src/geom/Polygon.cpp b/src/geom/Polygon.cpp
index 806b3503f..0a340c6c8 100644
--- a/src/geom/Polygon.cpp
+++ b/src/geom/Polygon.cpp
@@ -19,6 +19,7 @@
  **********************************************************************/
 
 #include <geos/algorithm/Area.h>
+#include <geos/algorithm/CurveToLineParams.h>
 #include <geos/algorithm/Orientation.h>
 #include <geos/util.h>
 #include <geos/geom/Coordinate.h>
@@ -37,14 +38,10 @@
 #include <algorithm>
 #include <memory>
 
-#include "geos/algorithm/CurveToLineParams.h"
-
 #ifndef GEOS_DEBUG
 #define GEOS_DEBUG 0
 #endif
 
-//using namespace geos::algorithm;
-
 namespace geos {
 namespace geom { // geos::geom
 

commit ffbc5581587a4b2ad107b7aaa44a008fa068d3ca
Author: Daniel Baston <dbaston at gmail.com>
Date:   Wed Jun 17 16:05:43 2026 -0400

    CircularArc: remove interpolateZM function (duplicated code)

diff --git a/include/geos/algorithm/CircularArcs.h b/include/geos/algorithm/CircularArcs.h
index 0aa868e23..a563e8f12 100644
--- a/include/geos/algorithm/CircularArcs.h
+++ b/include/geos/algorithm/CircularArcs.h
@@ -53,6 +53,9 @@ public:
     static geom::CoordinateXY createPoint(const geom::CoordinateXY& center, double radius, double theta);
 
     /** Interpolate Z/M values for a point added to an arc.
+     *
+     * Interpolation will be performed between [p0, p1] or [p1, p2] depending on
+     * the position of the supplied point.
      *
      * @param seq CoordinateSequence containing the arc points
      * @param i0 Index of the first point in the arc
diff --git a/include/geos/geom/CircularArc.h b/include/geos/geom/CircularArc.h
index 662075ff3..a2a4dd151 100644
--- a/include/geos/geom/CircularArc.h
+++ b/include/geos/geom/CircularArc.h
@@ -158,11 +158,6 @@ public:
         return algorithm::Distance::pointToSegment(midpoint, p0(), p2());
     }
 
-    /// Interpolate Z/M values from the three points that define the arc.
-    /// Interpolation will be performed between [p0, p1] or [p1, p2] depending on
-    /// the position of the supplied point.
-    void interpolateZM(const CoordinateXY& pt, double& z, double& m) const;
-
     bool isCCW() const {
         return getOrientation() == algorithm::Orientation::COUNTERCLOCKWISE;
     }
diff --git a/src/geom/CircularArc.cpp b/src/geom/CircularArc.cpp
index c5022c612..038fd1d18 100644
--- a/src/geom/CircularArc.cpp
+++ b/src/geom/CircularArc.cpp
@@ -383,7 +383,8 @@ CircularArc::addLinearizedPoints(CoordinateSequence& seq, const algorithm::Curve
         CoordinateXYZM pt{geos::algorithm::CircularArcs::createPoint(center, radius, theta)};
 
         if (hasZ || hasM) {
-            interpolateZM(pt, pt.z, pt.m);
+            CircularArcs::interpolateZM(*getCoordinateSequence(), getCoordinatePosition(),
+                                        center, isCCW, pt, pt.z, pt.m);
         }
 
         seq.add(pt);
@@ -392,109 +393,6 @@ CircularArc::addLinearizedPoints(CoordinateSequence& seq, const algorithm::Curve
     seq.add(*getCoordinateSequence(), getCoordinatePosition() + 2, getCoordinatePosition() + 2);
 }
 
-static double
-interpolateValue(double a1, double a2, double frac)
-{
-    frac = std::clamp(frac, 0.0, 1.0);
-    if (std::isnan(a1)) {
-        return a2;
-    }
-    if (std::isnan(a2)) {
-        return a1;
-    }
-    return a1 + frac * (a2 - a1);
-}
-
-void
-CircularArc::interpolateZM(const CoordinateXY &pt, double &z, double &m) const
-{
-    using geom::Ordinate;
-
-    const CoordinateSequence& seq = *getCoordinateSequence();
-    std::size_t i0 = getCoordinatePosition();
-
-    // Read Z, M from control point
-    double z1, m1;
-    seq.applyAt(i0 + 1, [&z1, &m1](const auto& arcPt) {
-        z1 = arcPt.template get<Ordinate::Z>();
-        m1 = arcPt.template get<Ordinate::M>();
-    });
-    // Test point = control point?
-    // Take Z, M from the control point
-    if (p1().equals2D(pt)) {
-        z = z1;
-        m = m1;
-        return;
-    }
-
-    // Read Z, M from start point
-    double z0, m0;
-    seq.applyAt(i0, [&z0, &m0](const auto& arcPt) {
-        z0 = arcPt.template get<Ordinate::Z>();
-        m0 = arcPt.template get<Ordinate::M>();
-    });
-    // Test point = start point?
-    // Take Z, M from the start point
-    if (p0().equals2D(pt)) {
-        z = z0;
-        m = m0;
-        return;
-    }
-
-    // Read Z, M from end point
-    double z2, m2;
-    seq.applyAt(i0 + 2, [&z2, &m2](const auto& arcPt) {
-        z2 = arcPt.template get<Ordinate::Z>();
-        m2 = arcPt.template get<Ordinate::M>();
-    });
-    // Test point = end point?
-    // Take Z, M from the end point
-    if (p2().equals2D(pt)) {
-        z = z2;
-        m = m2;
-        return;
-    }
-
-    double norm_theta0 = theta0();
-    const double norm_theta1 = theta1();
-    double norm_theta2 = theta2();
-    const double theta = algorithm::CircularArcs::getAngle(pt, getCenter());
-
-    if (!isCCW()) {
-        std::swap(norm_theta0, norm_theta2);
-        std::swap(z0, z2);
-        std::swap(m0, m2);
-    }
-
-    if (std::isnan(z1)) {
-        // Interpolate between p0 /  p2
-        const double frac = algorithm::Angle::fractionCCW(theta, norm_theta0, norm_theta2);
-        z = interpolateValue(z0, z2, frac);
-    } else if (algorithm::Angle::isWithinCCW(theta, norm_theta0, norm_theta1)) {
-        // Interpolate between p0 / p1
-        const double frac = algorithm::Angle::fractionCCW(theta, norm_theta0, norm_theta1);
-        z = interpolateValue(z0, z1, frac);
-    } else {
-        // Interpolate between p1 / p2
-        const double frac = algorithm::Angle::fractionCCW(theta, norm_theta1, norm_theta2);
-        z = interpolateValue(z1, z2, frac);
-    }
-
-    if (std::isnan(m1)) {
-        // Interpolate between p0 /  p2
-        const double frac = algorithm::Angle::fractionCCW(theta, norm_theta0, norm_theta2);
-        m = interpolateValue(m0, m2, frac);
-    } else if (algorithm::Angle::isWithinCCW(theta, norm_theta0, norm_theta1)) {
-        // Interpolate between p0 / p1
-        const double frac = algorithm::Angle::fractionCCW(theta, norm_theta0, norm_theta1);
-        m = interpolateValue(m0, m1, frac);
-    } else {
-        // Interpolate between p1 / p2
-        const double frac = algorithm::Angle::fractionCCW(theta, norm_theta1, norm_theta2);
-        m = interpolateValue(m1, m2, frac);
-    }
-}
-
 bool
 CircularArc::isUpwardAtPoint(const CoordinateXY& q) const
 {

commit 8626c42e21a3f0acfd2d8cc01c0c1f2a2c9bba70
Author: Daniel Baston <dbaston at gmail.com>
Date:   Wed Jun 17 16:04:40 2026 -0400

    geos_unit: small fixes

diff --git a/tests/unit/algorithm/CurveBuilderTest.cpp b/tests/unit/algorithm/CurveBuilderTest.cpp
index 2e45df6d4..734b52a03 100644
--- a/tests/unit/algorithm/CurveBuilderTest.cpp
+++ b/tests/unit/algorithm/CurveBuilderTest.cpp
@@ -27,8 +27,6 @@ struct test_algorithm_curvebuilder_data {
         auto outCurve = linearized->getCurved(ltcParams);
         auto expected = reader_.read(wkt_expected);
 
-        std::cout << outCurve->toString() << std::endl;
-
         double distanceTolerance = 1e-4;
 
         ensure_equals_exact_geometry_xyzm(outCurve.get(), expected.get(), distanceTolerance);
diff --git a/tests/unit/capi/GEOSClusterTest.cpp b/tests/unit/capi/GEOSClusterTest.cpp
index bb9b91449..ccc29be28 100644
--- a/tests/unit/capi/GEOSClusterTest.cpp
+++ b/tests/unit/capi/GEOSClusterTest.cpp
@@ -4,7 +4,6 @@
 #include <tut/tut.hpp>
 // geos
 #include <geos_c.h>
-#include <iostream>
 
 #include "capi_test_utils.h"
 
@@ -202,18 +201,21 @@ void object::test<3>()
 
     {
         GEOSClusterInfo* clusters = GEOSClusterDBSCAN_r(ctxt_, input_, 0.9, 0);
+        ensure(clusters);
         ensure_equals("DBSCAN", GEOSClusterInfo_getNumClusters_r(ctxt_, clusters), 3u);
         GEOSClusterInfo_destroy_r(ctxt_, clusters);
     }
 
     {
         GEOSClusterInfo* clusters = GEOSClusterGeometryDistance_r(ctxt_, input_, 0.9);
+        ensure(clusters);
         ensure_equals("GeometryDistance", GEOSClusterInfo_getNumClusters_r(ctxt_, clusters), 3u);
         GEOSClusterInfo_destroy_r(ctxt_, clusters);
     }
 
     {
         GEOSClusterInfo* clusters = GEOSClusterGeometryIntersects_r(ctxt_, input_);
+        ensure(clusters);
         ensure_equals("GeometryIntersects", GEOSClusterInfo_getNumClusters_r(ctxt_, clusters), 3u);
         GEOSClusterInfo_destroy_r(ctxt_, clusters);
     }
diff --git a/tests/unit/capi/GEOSConstrainedDelaunayTriangulationTest.cpp b/tests/unit/capi/GEOSConstrainedDelaunayTriangulationTest.cpp
index 649a442d8..2a768303d 100644
--- a/tests/unit/capi/GEOSConstrainedDelaunayTriangulationTest.cpp
+++ b/tests/unit/capi/GEOSConstrainedDelaunayTriangulationTest.cpp
@@ -72,7 +72,7 @@ template<>
 template<>
 void object::test<4>()
 {
-   set_test_name("curved inputs");
+    set_test_name("curved inputs");
     useContext();
 
     input_ = fromWKT("CURVEPOLYGON (COMPOUNDCURVE (CIRCULARSTRING (0 0, 1 1, 2 0), (2 0, 0 0)))");
diff --git a/tests/unit/capi/GEOSEqualsExactTest.cpp b/tests/unit/capi/GEOSEqualsExactTest.cpp
index 9c0671dd7..63066e39e 100644
--- a/tests/unit/capi/GEOSEqualsExactTest.cpp
+++ b/tests/unit/capi/GEOSEqualsExactTest.cpp
@@ -1,5 +1,5 @@
 //
-// Test Suite for C-API GEOSEquals
+// Test Suite for C-API GEOSEqualsExact
 
 #include <tut/tut.hpp>
 // geos
diff --git a/tests/unit/capi/GEOSisRingTest.cpp b/tests/unit/capi/GEOSisRingTest.cpp
index 25a9c31fd..cb6640622 100644
--- a/tests/unit/capi/GEOSisRingTest.cpp
+++ b/tests/unit/capi/GEOSisRingTest.cpp
@@ -86,7 +86,7 @@ void object::test<6>
     geom1_ = fromWKT("COMPOUNDCURVE (CIRCULARSTRING (0 0, 1 1, 2 0), (2 0, 0 0))");
     ensure(geom1_);
 
-    //ensure_equals(GEOSisRing_r(ctxt_, geom1_), 2);
+    ensure_equals(GEOSisRing_r(ctxt_, geom1_), 2);
     useCurveConversion();
     ensure_equals(GEOSisRing_r(ctxt_, geom1_), 1);
 }
diff --git a/tests/unit/geom/CircularStringTest.cpp b/tests/unit/geom/CircularStringTest.cpp
index f8d0ad5c4..59f0de931 100644
--- a/tests/unit/geom/CircularStringTest.cpp
+++ b/tests/unit/geom/CircularStringTest.cpp
@@ -442,9 +442,13 @@ void object::test<20>() {
 template<>
 template<>
 void object::test<21>() {
-    //set_test_name("liblwgeom: ticket #4058, big radius, small tolerance");
-    //checkLinearizeMaxDeviation("CIRCULARSTRING(2696000.553 1125699.831999999936670, 2695950.552000000141561 1125749.833000000100583, 2695865.195999999996275 1125835.189000)",
-    //"LINESTRING(2696000 1125700,2695932 1125768,2695866 1125836)", 0.0001);
+    set_test_name("liblwgeom: ticket #4058, big radius, small tolerance");
+
+    const auto params = CurveToLineParams::maxDeviation(0.0001);
+    auto g = wktreader_.read("CIRCULARSTRING(2696000.553 1125699.831999999936670, 2695950.552000000141561 1125749.833000000100583, 2695865.195999999996275 1125835.189000)");
+
+    auto ls = g->getLinearized(params);
+    ensure_equals("length is not equal", g->getLength(), ls->getLength(), 1e-3);
 }
 
 template<>
diff --git a/tests/unit/geom/CompoundCurveTest.cpp b/tests/unit/geom/CompoundCurveTest.cpp
index a156a45d0..79956e095 100644
--- a/tests/unit/geom/CompoundCurveTest.cpp
+++ b/tests/unit/geom/CompoundCurveTest.cpp
@@ -3,6 +3,7 @@
 #include <tut/tut_macros.hpp>
 
 #include <geos/algorithm/CurveToLineParams.h>
+#include <geos/algorithm/LineToCurveParams.h>
 #include <geos/geom/Coordinate.h>
 #include <geos/geom/CoordinateFilter.h>
 #include <geos/geom/GeometryFactory.h>
@@ -14,7 +15,6 @@
 #include <geos/operation/valid/RepeatedPointTester.h>
 #include <geos/util.h>
 
-#include "geos/algorithm/LineToCurveParams.h"
 
 
 using geos::algorithm::CurveToLineParams;
@@ -429,8 +429,8 @@ void object::test<12>()
     auto ls = cc->getLinearized(CurveToLineParams::stepSizeDegrees(90.0 / 4));
 
     ensure_equals(ls->getGeometryTypeId(), geos::geom::GEOS_LINESTRING);
-    ensure_equals(static_cast<geos::geom::Curve*>(cc.get())->getLinearized(CurveToLineParams::stepSizeDegrees(90.0 / 4))->getGeometryTypeId(), geos::geom::GEOS_LINESTRING);
-    ensure_equals(static_cast<CompoundCurve*>(ls.get())->getLinearized(CurveToLineParams::stepSizeDegrees(90.0 / 4))->getGeometryTypeId(), geos::geom::GEOS_LINESTRING);
+    ensure_equals(static_cast<Curve*>(cc.get())->getLinearized(CurveToLineParams::stepSizeDegrees(90.0 / 4))->getGeometryTypeId(), geos::geom::GEOS_LINESTRING);
+    ensure_equals(static_cast<LineString*>(ls.get())->getLinearized(CurveToLineParams::stepSizeDegrees(90.0 / 4))->getGeometryTypeId(), geos::geom::GEOS_LINESTRING);
 
     geos::operation::valid::RepeatedPointTester rpt;
     ensure(!rpt.hasRepeatedPoint(ls.get()));
diff --git a/tests/unit/geom/MultiPolygonTest.cpp b/tests/unit/geom/MultiPolygonTest.cpp
index 465828688..7722be4a4 100644
--- a/tests/unit/geom/MultiPolygonTest.cpp
+++ b/tests/unit/geom/MultiPolygonTest.cpp
@@ -98,7 +98,7 @@ void object::test<6>()
 
     auto params = geos::algorithm::LineToCurveParams();
 
-    // Tolerance to small to allow conversion to MultiSurface
+    // Tolerance too small to allow conversion to MultiSurface
     params.setRadiusTolerance(1e-12);
     ensure_equals(input->getCurved(params)->getGeometryTypeId(), geos::geom::GeometryTypeId::GEOS_MULTIPOLYGON);
 

commit 61dbd1a974403a09e33e6b10402cb9d5a3bf4e05
Author: Daniel Baston <dbaston at gmail.com>
Date:   Wed Mar 4 09:49:00 2026 -0500

    C API: Add automatic curve conversion

diff --git a/capi/CMakeLists.txt b/capi/CMakeLists.txt
index 63c2d02ee..804a10f52 100644
--- a/capi/CMakeLists.txt
+++ b/capi/CMakeLists.txt
@@ -31,6 +31,16 @@ set(VERSION_MAJOR ${GEOS_VERSION_MAJOR})
 set(VERSION_MINOR ${GEOS_VERSION_MINOR})
 set(VERSION_PATCH ${GEOS_VERSION_PATCH})
 
+set(INPUT_CURVES_CONVERTED_TO_LINES "This function does not support curved types directly. If curve-to-line
+* conversion parameters are registered with the context, curved
+* inputs will be automatically converted to lines. Otherwise, the
+* operation will fail.")
+set(OUTPUT_LINES_CONVERTED_TO_CURVES "This function outputs linear geometry types only. If line-to-curve
+* conversion parameters are registered with the context, and a 
+* curved input was provided, outputs will be automatically 
+* converted to curved types.")
+
+
 configure_file(
   ${CMAKE_CURRENT_LIST_DIR}/geos_c.h.in
   ${CMAKE_CURRENT_BINARY_DIR}/geos_c.h
diff --git a/capi/geos_c.h.in b/capi/geos_c.h.in
index 020769d9d..b88f41e90 100644
--- a/capi/geos_c.h.in
+++ b/capi/geos_c.h.in
@@ -509,6 +509,38 @@ extern GEOSMessageHandler_r GEOS_DLL GEOSContext_setErrorMessageHandler_r(
     GEOSMessageHandler_r ef,
     void *userData);
 
+/**
+* Set parameters to use for converting input curves to lines.
+*
+* These parameters will be used with this context whenever a curved geometry is
+* provided to a function that only supports linear types. If `params` is NULL,
+* no automatic conversion will be performed.
+*
+* \param extHandle the GEOS context
+* \param params the curve conversion parameters
+*
+* \since 3.15
+*/
+extern void GEOS_DLL GEOSContext_setCurveToLineParams_r(
+    GEOSContextHandle_t extHandle,
+    const GEOSCurveToLineParams* params);
+
+/**
+* Set parameters to use for converting output lines to curves.
+*
+* These parameters will be used with this context whenever a curved geometry is
+* provided to a function that only supports linear types. If `params` is NULL,
+* no automatic conversion will be performed.
+*
+* \param extHandle the GEOS context
+* \param params the curve conversion parameters
+*
+* \since 3.15
+*/
+extern void GEOS_DLL GEOSContext_setLineToCurveParams_r(
+    GEOSContextHandle_t extHandle,
+    const GEOSLineToCurveParams* params);
+
 /* ========== Coordinate Sequence functions ========== */
 
 /** \see GEOSCoordSeq_create */
@@ -3527,6 +3559,9 @@ and geometric quality.
 * Tests whether the input geometry is "simple". Mostly relevant for
 * linestrings. A "simple" linestring has no self-intersections.
 * Z and M values are not considered.
+*
+* @INPUT_CURVES_CONVERTED_TO_LINES@
+*
 * \param g The geometry to test
 * \return 1 on true, 0 on false, 2 on exception
 * \since 2.2
@@ -3540,6 +3575,8 @@ extern char GEOS_DLL GEOSisSimple(const GEOSGeometry* g);
 * Caller has the responsibility to destroy 'location' with
 * GEOSGeom_destroy()
 *
+* @INPUT_CURVES_CONVERTED_TO_LINES@
+*
 * \param g The geometry to test
 * \param findAllLocations Whether to return all self-intersection locations, or just one
 * \param locations A pointer in which the location GEOSGeometry will be placed
@@ -3562,6 +3599,8 @@ extern char GEOS_DLL GEOSisSimpleDetail(
 *
 * Z and M values are not considered.
 *
+* @INPUT_CURVES_CONVERTED_TO_LINES@
+*
 * \param g The geometry to test
 * \return 1 on true, 0 on false, 2 on exception
 * \see geos::operation::valid::isValidOp
@@ -3572,6 +3611,9 @@ extern char GEOS_DLL GEOSisValid(const GEOSGeometry* g);
 /**
 * Return the human readable reason a geometry is invalid,
 * "Valid Geometry" string otherwise, or NULL on exception.
+*
+* @INPUT_CURVES_CONVERTED_TO_LINES@
+*
 * \param g The geometry to test
 * \return A string with the reason, NULL on exception.
 *         Caller must GEOSFree() their result.
@@ -3586,6 +3628,9 @@ extern char GEOS_DLL *GEOSisValidReason(const GEOSGeometry *g);
 * rules are broken.
 * Caller has the responsibility to destroy 'reason' with GEOSFree()
 * and 'location' with GEOSGeom_destroy()
+*
+* @INPUT_CURVES_CONVERTED_TO_LINES@
+*
 * \param g The geometry to test
 * \param flags A value from the \ref GEOSValidFlags enum
 * \param reason A pointer in which the reason string will be places
@@ -3602,6 +3647,10 @@ extern char GEOS_DLL GEOSisValidDetail(
 
 /**
 * Repair an invalid geometry, returning a valid output.
+*
+* @INPUT_CURVES_CONVERTED_TO_LINES@
+* @OUTPUT_LINES_CONVERTED_TO_CURVES@
+*
 * \param g The geometry to repair
 * \return The repaired geometry. Caller must free with GEOSGeom_destroy().
 *
@@ -3613,6 +3662,10 @@ extern GEOSGeometry GEOS_DLL *GEOSMakeValid(
 /**
 * Repair an invalid geometry, returning a valid output, using the
 * indicated GEOSMakeValidMethods algorithm and options.
+*
+* @INPUT_CURVES_CONVERTED_TO_LINES@
+* @OUTPUT_LINES_CONVERTED_TO_CURVES@
+*
 * \param g is the geometry to test.
 * \param makeValidParams is a GEOSMakeValidParams with the desired parameters set on it.
 * \return A repaired geometry. Caller must free with GEOSGeom_destroy().
@@ -3683,6 +3736,8 @@ extern int GEOS_DLL GEOSMakeValidParams_setKeepCollapsed(
 * If the minimum clearance cannot be defined for a geometry (such as with a single point, or a multipoint
 * whose points are identical, a value of Infinity will be calculated.
 *
+* @INPUT_CURVES_CONVERTED_TO_LINES@
+*
 * \param g the input geometry
 * \param d a double to which the result can be stored
 * \return 0 if no exception occurred.
@@ -3697,6 +3752,8 @@ extern int GEOS_DLL GEOSMinimumClearance(const GEOSGeometry* g, double* d);
 * Returns a 2D LineString whose endpoints define the minimum clearance of a geometry.
 * If the geometry has no minimum clearance, an empty LineString will be returned.
 *
+* @INPUT_CURVES_CONVERTED_TO_LINES@
+*
 * \param g the input geometry
 * \return a linestring geometry, or NULL if an exception occurred.
 * Caller is responsible for freeing with GEOSGeom_destroy().
@@ -3800,6 +3857,8 @@ extern int GEOS_DLL GEOSGeomGetLength(
 * Calculate the distance between two geometries.
 * Distance is calculated in the XY plane; Z and M values are ignored.
 *
+* @INPUT_CURVES_CONVERTED_TO_LINES@
+*
 * \param[in] g1 Input geometry
 * \param[in] g2 Input geometry
 * \param[out] dist Pointer to be filled in with distance result. Positive
@@ -3817,6 +3876,8 @@ extern int GEOS_DLL GEOSDistance(
 * within the given dist.
 * Distance is calculated in the XY plane; Z and M values are ignored.
 *
+* @INPUT_CURVES_CONVERTED_TO_LINES@
+*
 * \param g1 Input geometry
 * \param g2 Input geometry
 * \param dist The max distance
@@ -3834,6 +3895,9 @@ extern char GEOS_DLL GEOSDistanceWithin(
 * indexed facet distance, which first indexes the geometries
 * internally, then calculates the distance. Useful when one
 * or both geometries is very large.
+*
+* @INPUT_CURVES_CONVERTED_TO_LINES@
+*
 * \param[in] g1 Input geometry
 * \param[in] g2 Input geometry
 * \param[out] dist Pointer to be filled in with distance result
@@ -3852,6 +3916,8 @@ extern int GEOS_DLL GEOSDistanceIndexed(
 * The first point comes from g1 geometry and the second point comes from g2.
 * Returned points will not have Z or M values.
 *
+* @INPUT_CURVES_CONVERTED_TO_LINES@
+*
 * \param[in] g1 Input geometry
 * \param[in] g2 Input geometry
 * \return A coordinate sequence with the two points, or NULL on exception.
@@ -3867,6 +3933,9 @@ extern GEOSCoordSequence GEOS_DLL *GEOSNearestPoints(
 * Calculate the Hausdorff distance between two geometries.
 * [Hausdorff distance](https://en.wikipedia.org/wiki/Hausdorff_distance)
 * is the largest distance between two geometries.
+*
+* @INPUT_CURVES_CONVERTED_TO_LINES@
+*
 * \param[in] g1 Input geometry
 * \param[in] g2 Input geometry
 * \param[out] dist Pointer to be filled in with distance result
@@ -3883,6 +3952,9 @@ extern int GEOS_DLL GEOSHausdorffDistance(
 /**
 * Calculate the Hausdorff distance between two geometries and return
 * the coordinates of the points that determine the distance.
+*
+* @INPUT_CURVES_CONVERTED_TO_LINES@
+*
 * \param[in] g1 Input geometry
 * \param[in] g2 Input geometry
 * \param[out] dist Pointer to be filled in with distance result
@@ -3908,6 +3980,9 @@ extern int GEOS_DLL GEOSHausdorffDistanceWithPoints(
 * by densifying the inputs before computation.
 * [Hausdorff distance](https://en.wikipedia.org/wiki/Hausdorff_distance)
 * is the largest distance between two geometries.
+*
+* @INPUT_CURVES_CONVERTED_TO_LINES@
+*
 * \param[in] g1 Input geometry
 * \param[in] g2 Input geometry
 * \param[in] densifyFrac The largest % of the overall line length that
@@ -3924,24 +3999,27 @@ extern int GEOS_DLL GEOSHausdorffDistanceDensify(
     double *dist);
 
 /**
- * Calculate a more precise Hausdorff distance between two geometries,
- * by densifying the inputs before computation and return the points
- * that determine the distance.
- * [Hausdorff distance](https://en.wikipedia.org/wiki/Hausdorff_distance)
- * is the largest distance between two geometries.
- * \param[in] g1 Input geometry
- * \param[in] g2 Input geometry
- * \param[in] densifyFrac The largest % of the overall line length that
- * any given two point segment should be.
- * \param[out] dist Pointer to be filled in with distance result
- * \param[out] p1x X coordinate of point on g1
- * \param[out] p1y Y coordinate of point on g1
- * \param[out] p2x X coordinate of point on g2
- * \param[out] p2y Y coordinate of point on g2
- * \return 1 on success, 0 on exception.
- * \see goes::algorithm::distance::DiscreteHausdorffDistance
- * \since 3.15
- */
+* Calculate a more precise Hausdorff distance between two geometries,
+* by densifying the inputs before computation and return the points
+* that determine the distance.
+* [Hausdorff distance](https://en.wikipedia.org/wiki/Hausdorff_distance)
+* is the largest distance between two geometries.
+*
+* @INPUT_CURVES_CONVERTED_TO_LINES@
+*
+* \param[in] g1 Input geometry
+* \param[in] g2 Input geometry
+* \param[in] densifyFrac The largest % of the overall line length that
+* any given two point segment should be.
+* \param[out] dist Pointer to be filled in with distance result
+* \param[out] p1x X coordinate of point on g1
+* \param[out] p1y Y coordinate of point on g1
+* \param[out] p2x X coordinate of point on g2
+* \param[out] p2y Y coordinate of point on g2
+* \return 1 on success, 0 on exception.
+* \see goes::algorithm::distance::DiscreteHausdorffDistance
+* \since 3.15
+*/
 extern int GEOS_DLL GEOSHausdorffDistanceDensifyWithPoints(
     const GEOSGeometry *g1,
     const GEOSGeometry *g2,
@@ -3955,6 +4033,9 @@ extern int GEOS_DLL GEOSHausdorffDistanceDensifyWithPoints(
 * [Frechet distance](https://en.wikipedia.org/wiki/Fr%C3%A9chet_distance)
 * between two geometries,
 * a similarity measure for linear features.
+*
+* @INPUT_CURVES_CONVERTED_TO_LINES@
+*
 * \param[in] g1 Input geometry
 * \param[in] g2 Input geometry
 * \param[out] dist Pointer to be filled in with distance result
@@ -3974,6 +4055,9 @@ extern int GEOS_DLL GEOSFrechetDistance(
 * between two geometries,
 * a similarity measure for linear features.
 * The inputs can be densified to provide a more accurate result.
+*
+* @INPUT_CURVES_CONVERTED_TO_LINES@
+*
 * \param[in] g1 Input geometry
 * \param[in] g2 Input geometry
 * \param[in] densifyFrac The largest % of the overall line length that
@@ -4001,6 +4085,9 @@ extern int GEOS_DLL GEOSFrechetDistanceDensify(
 
 /**
 * Distance of point projected onto line from the start of the line.
+*
+* @INPUT_CURVES_CONVERTED_TO_LINES@
+*
 * \param line linear target of projection
 * \param point point to be projected onto 'g'
 * \return distance along line that point projects to, -1 on exception
@@ -4017,7 +4104,9 @@ extern double GEOS_DLL GEOSProject(const GEOSGeometry* line,
 * If applicable, the Z value of the returned point will be interpolated from the input line.
 * Since GEOS 3.15, the M value will also be interpolated.
 *
-* If the input is not a LineString, NULL will be returned.
+* @INPUT_CURVES_CONVERTED_TO_LINES@
+*
+* If the (converted) input is not a LineString, NULL will be returned.
 *
 * \param line linear target of projection
 * \param d distance from start of line to created point
@@ -4032,6 +4121,9 @@ extern GEOSGeometry GEOS_DLL *GEOSInterpolate(const GEOSGeometry* line,
 * Project point to line and calculate the **proportion** of
 * the line the point is from the start. For example, a point that
 * projects to the middle of a line would be return 0.5.
+*
+* @INPUT_CURVES_CONVERTED_TO_LINES@
+*
 * \param line linear target of projection
 * \param point the point to project
 * \return The proportion of the overall line length that the projected
@@ -4048,7 +4140,9 @@ extern double GEOS_DLL GEOSProjectNormalized(const GEOSGeometry* line,
 * If applicable, the Z value of the returned point will be interpolated from the input line.
 * Since GEOS 3.15, the M value will also be interpolated.
 *
-* If the input is not a LineString, NULL will be returned.
+* @INPUT_CURVES_CONVERTED_TO_LINES@
+*
+* If the (converted) input is not a LineString, NULL will be returned.
 *
 * \param line linear target of projection
 * \param proportion The proportion from the start of line to created point
@@ -4072,6 +4166,10 @@ extern GEOSGeometry GEOS_DLL *GEOSInterpolateNormalized(
 /**
 * Returns the intersection of two geometries: the set of points
 * that fall within **both** geometries.
+*
+* @INPUT_CURVES_CONVERTED_TO_LINES@
+* @OUTPUT_LINES_CONVERTED_TO_CURVES@
+*
 * \param g1 one of the geometries
 * \param g2 the other geometry
 * \return A newly allocated geometry of the intersection. NULL on exception.
@@ -4100,6 +4198,10 @@ extern GEOSGeometry GEOS_DLL *GEOSIntersectionPrec(const GEOSGeometry* g1, const
 /**
 * Returns the difference of two geometries A and B: the set of points
 * that fall within A but **not** within B.
+*
+* @INPUT_CURVES_CONVERTED_TO_LINES@
+* @OUTPUT_LINES_CONVERTED_TO_CURVES@
+*
 * \param ga the base geometry
 * \param gb the geometry to subtract from it
 * \return A newly allocated geometry of the difference. NULL on exception.
@@ -4135,6 +4237,10 @@ extern GEOSGeometry GEOS_DLL *GEOSDifferencePrec(
 * Returns the symmetric difference of two geometries A and B: the set of points
 * that fall in A but **not** within B and the set of points that fall in B but
 * **not** in A.
+*
+* @INPUT_CURVES_CONVERTED_TO_LINES@
+* @OUTPUT_LINES_CONVERTED_TO_CURVES@
+*
 * \param ga geometry A
 * \param gb geometry B
 * \return A newly allocated geometry of the symmetric difference. NULL on exception.
@@ -4170,6 +4276,10 @@ extern GEOSGeometry GEOS_DLL *GEOSSymDifferencePrec(
 /**
 * Returns the union of two geometries A and B: the set of points
 * that fall in A **or** within B.
+*
+* @INPUT_CURVES_CONVERTED_TO_LINES@
+* @OUTPUT_LINES_CONVERTED_TO_CURVES@
+*
 * \param ga geometry A
 * \param gb geometry B
 * \return A newly allocated geometry of the union. NULL on exception.
@@ -4205,6 +4315,10 @@ extern GEOSGeometry GEOS_DLL *GEOSUnionPrec(
 * Returns the union of all components of a single geometry. Usually
 * used to convert a collection into the smallest set of polygons
 * that cover the same area.
+*
+* @INPUT_CURVES_CONVERTED_TO_LINES@
+* @OUTPUT_LINES_CONVERTED_TO_CURVES@
+*
 * \param g The input geometry
 * \return A newly allocated geometry of the union. NULL on exception.
 * Caller is responsible for freeing with GEOSGeom_destroy().
@@ -4237,6 +4351,10 @@ extern GEOSGeometry GEOS_DLL *GEOSUnaryUnionPrec(
 * Optimized union algorithm for inputs that can be divided into subsets
 * that do not intersect. If there is only one such subset, performance
 * can be expected to be worse than GEOSUnionaryUnion.
+*
+* @INPUT_CURVES_CONVERTED_TO_LINES@
+* @OUTPUT_LINES_CONVERTED_TO_CURVES@
+*
 * \param g The input geometry
 * \return A newly allocated geometry of the union, or NULL on exception.
 * Caller is responsible for freeing with GEOSGeom_destroy().
@@ -4249,6 +4367,10 @@ extern GEOSGeometry GEOS_DLL *GEOSDisjointSubsetUnion(const GEOSGeometry *g);
 * Intersection optimized for a rectangular clipping polygon.
 * Supposed to be faster than using GEOSIntersection(). Not
 * guaranteed to return valid results.
+*
+* @INPUT_CURVES_CONVERTED_TO_LINES@
+* @OUTPUT_LINES_CONVERTED_TO_CURVES@
+*
 * \param g The input geometry to be clipped
 * \param xmin Left bound of clipping rectangle
 * \param ymin Lower bound of clipping rectangle
@@ -4268,6 +4390,10 @@ extern GEOSGeometry GEOS_DLL *GEOSClipByRect(
 /**
 * Compute the intersection of a geometry with each polygon in
 * a rectangular grid.
+*
+* @INPUT_CURVES_CONVERTED_TO_LINES@
+* @OUTPUT_LINES_CONVERTED_TO_CURVES@
+*
 * \param g The input geometry to be clipped
 * \param xmin Left bound of grd
 * \param ymin Lower bound of grid
@@ -4298,6 +4424,9 @@ extern GEOSGeometry GEOS_DLL *GEOSSubdivideByGrid(
 /**
 * Determine the fraction of each cell in a rectangular grid
 * that is covered by a polygon.
+*
+* @INPUT_CURVES_CONVERTED_TO_LINES@
+*
 * \param g The input geometry
 * \param xmin Left bound of grd
 * \param ymin Lower bound of grid
@@ -4328,6 +4457,8 @@ extern int GEOS_DLL GEOSGridIntersectionFractions(
 * - second element is a MultiLineString containing shared paths
 *   having the _opposite_ direction on the two inputs
 *
+* @INPUT_CURVES_CONVERTED_TO_LINES@
+*
 * \param g1 An input geometry
 * \param g2 An input geometry
 * \return The shared paths
@@ -4352,36 +4483,42 @@ static const size_t GEOS_CLUSTER_NONE = (size_t) -1;
 
 ///@{
 /**
- * @brief GEOSClusterDBSCAN
- *
- * Cluster geometries using the DBSCAN algorithm
- *
- * @param g a collection of geometries to be clustered
- * @param eps distance parameter for clustering
- * @param minPoints density parameter for clustering
- * @return cluster information object
- */
+* @brief GEOSClusterDBSCAN
+*
+* Cluster geometries using the DBSCAN algorithm
+*
+* @INPUT_CURVES_CONVERTED_TO_LINES@
+*
+* @param g a collection of geometries to be clustered
+* @param eps distance parameter for clustering
+* @param minPoints density parameter for clustering
+* @return cluster information object
+*/
 extern GEOSClusterInfo GEOS_DLL* GEOSClusterDBSCAN(const GEOSGeometry* g, double eps, unsigned minPoints);
 
 /**
- * @brief GEOSClusterGeometryDistance
- *
- * Cluster geometries according to a distance threshold
- *
- * @param g a collection of geometries to be clustered
- * @param d minimum distance between geometries in the same cluster
- * @return cluster information object
- */
+* @brief GEOSClusterGeometryDistance
+*
+* Cluster geometries according to a distance threshold
+*
+* @INPUT_CURVES_CONVERTED_TO_LINES@
+*
+* @param g a collection of geometries to be clustered
+* @param d minimum distance between geometries in the same cluster
+* @return cluster information object
+*/
 extern GEOSClusterInfo GEOS_DLL* GEOSClusterGeometryDistance(const GEOSGeometry* g, double d);
 
 /**
- * @brief GEOSClusterGeometryIntersects
- *
- * Cluster geometries that intersect
- *
- * @param g a collection of geometries to be clustered
- * @return cluster information object
- */
+* @brief GEOSClusterGeometryIntersects
+*
+* Cluster geometries that intersect
+*
+* @INPUT_CURVES_CONVERTED_TO_LINES@
+*
+* @param g a collection of geometries to be clustered
+* @return cluster information object
+*/
 extern GEOSClusterInfo GEOS_DLL* GEOSClusterGeometryIntersects(const GEOSGeometry* g);
 
 /**
@@ -4389,6 +4526,8 @@ extern GEOSClusterInfo GEOS_DLL* GEOSClusterGeometryIntersects(const GEOSGeometr
  *
  * Cluster geometries according to an envelope distance threshold
  *
+ * This function supports curved geometry types.
+ *
  * @param g a collection of geometries to be clustered
  * @param d minimum envelope distance between geometries in the same cluster
  * @return cluster information object
@@ -4400,6 +4539,8 @@ extern GEOSClusterInfo GEOS_DLL* GEOSClusterEnvelopeDistance(const GEOSGeometry*
  *
  * Cluster geometries whose envelopes intersect
  *
+ * This function supports curved geometry types.
+ *
  * @param g
  * @return cluster information object
  */
@@ -4468,6 +4609,8 @@ extern void GEOS_DLL GEOSClusterInfo_destroy(GEOSClusterInfo* clusters);
 * Buffer a geometry.
 * The coordinates of the constructed geometry will not have Z or M values.
 *
+* @INPUT_CURVES_CONVERTED_TO_LINES@
+*
 * \param g The input geometry to be buffered.
 * \param width The distance by which to expand the geometry (or contract)
 *        if the value is negative.
@@ -4565,6 +4708,9 @@ extern int GEOS_DLL GEOSBufferParams_setSingleSided(
 
 /**
 * Generates a buffer using the special parameters in the GEOSBufferParams
+* 
+* @INPUT_CURVES_CONVERTED_TO_LINES@
+*
 * \param g The geometry to buffer
 * \param p The parameters to apply to the buffer process
 * \param width The buffer distance
@@ -4582,6 +4728,8 @@ extern GEOSGeometry GEOS_DLL *GEOSBufferWithParams(
 * Generate a buffer using the provided style parameters.
 * The coordinates of the constructed geometry will not have Z or M values.
 *
+* @INPUT_CURVES_CONVERTED_TO_LINES@
+*
 * \param g The geometry to buffer
 * \param width Width of the buffer
 * \param quadsegs Number of segments per quadrant
@@ -4606,6 +4754,8 @@ extern GEOSGeometry GEOS_DLL *GEOSBufferWithStyle(
 * Handles all non-curved geometry types as input.
 * The coordinates of the constructed geometry will not have Z or M values.
 *
+* @INPUT_CURVES_CONVERTED_TO_LINES@
+*
 * - For a LineString the result is a LineString.
 * - For a Point the result is an empty LineString.
 * - For a Polygon the result is the boundary lines(s) of the polygon buffered to the offset distance
@@ -4642,6 +4792,10 @@ extern GEOSGeometry GEOS_DLL *GEOSOffsetCurve(const GEOSGeometry* g,
 * noded and do not overlap. It may generate an error (return NULL)
 * for inputs that do not satisfy this constraint, however this is not
 * guaranteed.
+*
+* @INPUT_CURVES_CONVERTED_TO_LINES@
+* @OUTPUT_LINES_CONVERTED_TO_CURVES@
+*
 * \param g The input geometry
 * \return A geometry that covers all the points of the input geometry.
 * Caller is responsible for freeing with GEOSGeom_destroy().
@@ -4656,6 +4810,8 @@ extern GEOSGeometry GEOS_DLL *GEOSCoverageUnion(const GEOSGeometry *g);
 * with exactly matching edge geometry) to find places where the
 * assumption of exactly matching edges is not met.
 *
+* @INPUT_CURVES_CONVERTED_TO_LINES@
+*
 * \param input The polygonal coverage to access,
 *        stored in a geometry collection. All members must be POLYGON
 *        or MULTIPOLYGON.
@@ -4692,6 +4848,9 @@ extern int GEOS_DLL GEOSCoverageIsValid(
 * it will still be simplified, but invalid topology such as crossing
 * edges will still be invalid.
 *
+* @INPUT_CURVES_CONVERTED_TO_LINES@
+* @OUTPUT_LINES_CONVERTED_TO_CURVES@
+*
 * \param input The polygonal coverage to access,
 *        stored in a geometry collection. All members must be POLYGON
 *        or MULTIPOLYGON.
@@ -4824,6 +4983,9 @@ GEOSCoverageCleanParams_setOverlapMergeStrategy(
 * the input. Polygons that have collapsed during cleaning will be returned
 * as empties.
 *
+* @INPUT_CURVES_CONVERTED_TO_LINES@
+* @OUTPUT_LINES_CONVERTED_TO_CURVES@
+*
 * \param input The dirty polygonal coverage,
 *        stored in a geometry collection. All members must be POLYGON
 *        or MULTIPOLYGON.
@@ -4899,6 +5061,8 @@ extern GEOSGeometry GEOS_DLL *GEOSBoundary(const GEOSGeometry* g);
 * Z values in the input coordinates will be preserved.
 * M values in the input coordinates will not be preserved.
 *
+* @INPUT_CURVES_CONVERTED_TO_LINES@
+*
 * \param g The input geometry
 * \return A newly allocated geometry of the convex hull. NULL on exception.
 * Caller is responsible for freeing with GEOSGeom_destroy().
@@ -4926,6 +5090,8 @@ extern GEOSGeometry GEOS_DLL *GEOSConvexHull(const GEOSGeometry* g);
 * This can be expressed as a ratio between the lengths of the longest and shortest edges.
 * 1 produces the convex hull; 0 produces a hull with maximum concaveness
 *
+* @INPUT_CURVES_CONVERTED_TO_LINES@
+*
 * \param g The input geometry
 * \param ratio The edge length ratio value, between 0 and 1.
 * \param allowHoles When non-zero, the polygonal output may contain holes.
@@ -4960,9 +5126,11 @@ extern GEOSGeometry GEOS_DLL *GEOSConcaveHull(
 * determined by a numeric target parameter.
 * The concave hull is constructed by removing the longest outer edges
 * of the Delaunay Triangulation of the space between the polygons,
-* until the specified maximm edge length is reached.
+* until the specified maximum edge length is reached.
 * A large value produces the convex hull, 0 produces the hull of maximum concaveness.
 *
+* @INPUT_CURVES_CONVERTED_TO_LINES@
+*
 * \param g The input geometry
 * \param length The maximum edge length (0 or greater)
 * \param allowHoles When non-zero, the polygonal output may contain holes.
@@ -5001,6 +5169,8 @@ extern GEOSGeometry GEOS_DLL *GEOSConcaveHullByLength(
 * The input polygons *must* be a *valid* MultiPolygon
 * (i.e. they must be non-overlapping).
 *
+* @INPUT_CURVES_CONVERTED_TO_LINES@
+*
 * \param g the valid MultiPolygon geometry to process
 * \param lengthRatio specifies the Maximum Edge Length as a
 *        fraction of the difference between the longest and
@@ -5035,6 +5205,9 @@ extern GEOSGeometry GEOS_DLL *GEOSConcaveHullOfPolygons(
 * An outer hull is computed if the parameter is positive,
 * an inner hull is computed if it is negative.
 *
+* @INPUT_CURVES_CONVERTED_TO_LINES@
+* @OUTPUT_LINES_CONVERTED_TO_CURVES@
+*
 * \param g the polygonal geometry to process
 * \param isOuter indicates whether to compute an outer or inner hull (1 for outer hull, 0 for inner)
 * \param vertexNumFraction the target fraction of the count of input vertices to retain in result
@@ -5069,6 +5242,9 @@ enum GEOSPolygonHullParameterModes {
 * produces the original geometry.
 * Either outer or inner hulls can be computed.
 *
+* @INPUT_CURVES_CONVERTED_TO_LINES@
+* @OUTPUT_LINES_CONVERTED_TO_CURVES@
+*
 * \param g the polygonal geometry to process
 * \param isOuter indicates whether to compute an outer or inner hull (1 for outer hull, 0 for inner)
 * \param parameterMode the interpretation to apply to the parameter argument; see \ref GEOSPolygonHullParameterModes
@@ -5095,6 +5271,9 @@ extern GEOSGeometry GEOS_DLL *GEOSPolygonHullSimplifyMode(
 * the input is degenerate (a line or point) a linestring or point
 * is returned. The minimum rotated rectangle can be used as an
 * extremely generalized representation for the given geometry.
+*
+* @INPUT_CURVES_CONVERTED_TO_LINES@
+*
 * \param g The input geometry
 * \return A newly allocated geometry of the rotated envelope. NULL on exception.
 * Caller is responsible for freeing with GEOSGeom_destroy().
@@ -5119,6 +5298,9 @@ extern GEOSGeometry GEOS_DLL *GEOSMinimumRotatedRectangle(const GEOSGeometry* g)
 * way by using spatial indexes.
 * Returns a two-point linestring, with one point at the center of the inscribed circle and the other
 * on the boundary of the inscribed circle.
+*
+* @INPUT_CURVES_CONVERTED_TO_LINES@
+*
 * \param g Input geometry
 * \param tolerance Stop the algorithm when the search area is smaller than this tolerance
 * \return A newly allocated geometry of the MIC. NULL on exception.
@@ -5144,17 +5326,19 @@ extern GEOSGeometry GEOS_DLL *GEOSMaximumInscribedCircle(
 * the obstacles (up to the given distance tolerance).
 * The LEC is determined by the center point and a point indicating the circle radius
 * (which will lie on an obstacle).
-* \n
+*
 * To compute an LEC which lies **wholly** within a polygonal boundary,
 * include the boundary of the polygon(s) as a linear obstacle.
-* \n
+*
 * The implementation uses a successive-approximation technique over a grid of square cells covering the obstacles and boundary.
 * The grid is refined using a branch-and-bound algorithm.  Point containment and distance are computed in a performant
 * way by using spatial indexes.
-* \n
+*
 * Returns the LEC radius as a two-point linestring, with the start point at the center of the inscribed circle and the end
 * on the boundary of the circle.
 *
+* @INPUT_CURVES_CONVERTED_TO_LINES@
+*
 * \param obstacles The geometries that the LEC must not cross
 * \param boundary The area to contain the LEC center (may be null or empty)
 * \param tolerance Stop the algorithm when the search area is smaller than this tolerance
@@ -5175,6 +5359,9 @@ extern GEOSGeometry GEOS_DLL *GEOSLargestEmptyCircle(
 * contains the geometry, where a band is a strip of the plane defined
 * by two parallel lines. This can be thought of as the smallest hole that the geometry
 * can be moved through, with a single rotation.
+*
+* @INPUT_CURVES_CONVERTED_TO_LINES@
+*
 * \param g The input geometry
 * \return A newly allocated geometry of the LEC. NULL on exception.
 * Caller is responsible for freeing with GEOSGeom_destroy().
@@ -5187,6 +5374,9 @@ extern GEOSGeometry GEOS_DLL *GEOSMinimumWidth(const GEOSGeometry* g);
 
 /**
 * Returns a 2D point that is inside the boundary of a polygonal geometry.
+*
+* @INPUT_CURVES_CONVERTED_TO_LINES@
+*
 * \param g The input geometry
 * \return A point that is inside the input
 * Caller is responsible for freeing with GEOSGeom_destroy().
@@ -5198,6 +5388,9 @@ extern GEOSGeometry GEOS_DLL *GEOSPointOnSurface(const GEOSGeometry* g);
 
 /**
 * Returns a 2D point at the center of mass of the input.
+*
+* @INPUT_CURVES_CONVERTED_TO_LINES@
+*
 * \param g The input geometry
 * \return A point at the center of mass of the input
 * Caller is responsible for freeing with GEOSGeom_destroy().
@@ -5210,6 +5403,9 @@ extern GEOSGeometry GEOS_DLL *GEOSGetCentroid(const GEOSGeometry* g);
 /**
 * Returns a 2D geometry which represents the "minimum bounding circle",
 * the smallest circle that contains the input.
+*
+* @INPUT_CURVES_CONVERTED_TO_LINES@
+*
 * \param[in] g The input geometry
 * \param[out] radius Pointer will be filled with output radius.
 * \param[out] center Pointer will be filled with output circle center. Caller must free.
@@ -5230,6 +5426,9 @@ extern GEOSGeometry GEOS_DLL *GEOSMinimumBoundingCircle(
 * Z values from the input points will be propagated to the generated polygons.
 * M values from the input points will be ignored.
 *
+* Curved geometries are supported since GEOS 3.15. For curved geometries,
+* the control point and endpoints of each arc will be used as input vertices.
+*
 * \param g the input geometry whose vertices will be used as "sites"
 * \param tolerance optional snapping tolerance to use for improved robustness
 * \param onlyEdges if non-zero will return a MultiLineString, otherwise it will
@@ -5253,6 +5452,8 @@ extern GEOSGeometry GEOS_DLL * GEOSDelaunayTriangulation(
 * Z values from the input points will be propagated to the generated polygons.
 * M values from the input points will be ignored.
 *
+* @INPUT_CURVES_CONVERTED_TO_LINES@
+*
 * \param g the input geometry whose rings will be used as input
 * \return A newly allocated geometry. NULL on exception.
 * Caller is responsible for freeing with GEOSGeom_destroy().
@@ -5277,6 +5478,9 @@ enum GEOSVoronoiFlags
 * Returns the 2D Voronoi polygons or edges computed from the vertices
 * of the given geometry.
 *
+* Curved geometries are supported since GEOS 3.15. For curved geometries,
+* the control point and endpoints of each arc will be used as input vertices.
+*
 * \param g the input geometry whose vertices will be used as sites.
 * \param tolerance snapping tolerance to use for improved robustness. A
                    tolerance of 0.0 specifies that no snapping will take
@@ -5485,6 +5689,11 @@ extern GEOSGeometry GEOS_DLL *GEOSBuildArea(const GEOSGeometry* g);
 * evenly subdivide that segment. Z and M values of added
 * vertices will be interpolated.
 * Only linear components of input geometry are densified.
+* 
+* Curved geometries are not supported, unless curve-to-line parameters are
+* registered with the context handle. Output geometries will not contain
+* curves.
+*
 * \param g The geometry to densify
 * \param tolerance the distance tolerance to densify
 * \return The densified geometry, or NULL on exception.
@@ -5652,10 +5861,13 @@ extern GEOSGeometry GEOS_DLL *GEOSGeom_extractUniquePoints(
 /**
 * Calculate the
 * [Hilbert code](https://en.wikipedia.org/wiki/Hilbert_curve)
-* of the centroid of a geometry relative to an extent.
+* of the center of a geometry's bounding box relative to a larger extent.
 * This allows sorting geometries in a deterministic way, such that similar Hilbert codes are
 * likely to be near each other in two-dimensional space.
 * The caller must ensure that the geometry is contained within the extent.
+*
+* Curved geometries are supported since GEOS 3.13.
+*
 * \param[in] geom Input geometry, must be non-empty
 * \param[in] extent Extent within which to calculate the Hilbert code for geom
 * \param[in] level The level of precision of the Hilbert curve, up to 16
diff --git a/capi/geos_ts_c.cpp b/capi/geos_ts_c.cpp
index 3cf883518..63fbf96fe 100644
--- a/capi/geos_ts_c.cpp
+++ b/capi/geos_ts_c.cpp
@@ -133,6 +133,7 @@
 #include <cstring>
 #include <fstream>
 #include <iostream>
+#include <optional>
 #include <sstream>
 #include <string>
 #include <memory>
@@ -259,6 +260,8 @@ typedef struct GEOSContextHandle_HS {
     int WKBByteOrder;
     int initialized;
     std::unique_ptr<Point> point2d;
+    std::optional<GEOSLineToCurveParams> lineToCurveParams;
+    std::optional<GEOSCurveToLineParams> curveToLineParams;
 
     GEOSContextHandle_HS()
         :
@@ -395,6 +398,81 @@ typedef struct GEOSContextHandle_HS {
     }
 } GEOSContextHandleInternal_t;
 
+class InputGeometry {
+
+public:
+    explicit InputGeometry(const Geometry* geom) :
+        m_geom(const_cast<Geometry*>(geom)), m_owned(false) {}
+
+    explicit InputGeometry(std::unique_ptr<Geometry> geom) :
+        m_geom(geom.release()), m_owned(true) {}
+
+    ~InputGeometry() {
+        if (m_owned) {
+            delete m_geom;
+        }
+    }
+
+    operator const Geometry*() const {
+        return m_geom;
+    }
+
+    const Geometry* get() const {
+        return m_geom;
+    }
+
+    const Geometry* operator->() const {
+        return m_geom;
+    }
+
+    bool isLinearized() const {
+        return m_owned;
+    }
+
+    InputGeometry(const InputGeometry&) = delete;
+    InputGeometry& operator=(const InputGeometry&) = delete;
+
+    InputGeometry(InputGeometry&& other) noexcept : m_geom(other.m_geom), m_owned(other.m_owned) {
+        other.m_geom = nullptr;
+        other.m_owned = false;
+    }
+    InputGeometry& operator=(InputGeometry&& other) noexcept {
+        m_geom = other.m_geom;
+        m_owned = other.m_owned;
+        other.m_geom = nullptr;
+        other.m_owned = false;
+
+        return *this;
+    }
+
+private:
+    Geometry* m_geom;
+    bool m_owned;
+};
+
+static bool
+isCurvedType (geos::geom::GeometryTypeId typ) {
+    return typ == geos::geom::GeometryTypeId::GEOS_CIRCULARSTRING ||
+        typ == geos::geom::GeometryTypeId::GEOS_COMPOUNDCURVE ||
+        typ == geos::geom::GeometryTypeId::GEOS_CURVEPOLYGON ||
+        typ == geos::geom::GeometryTypeId::GEOS_MULTICURVE ||
+        typ == geos::geom::GeometryTypeId::GEOS_MULTISURFACE;
+}
+
+InputGeometry convertToLineIfNeeded(GEOSContextHandle_t extHandle, const Geometry* g) {
+    if (extHandle->curveToLineParams.has_value() && g != nullptr && (isCurvedType(g->getGeometryTypeId()) || g->hasCurvedComponents())) {
+        return InputGeometry(g->getLinearized(extHandle->curveToLineParams.value()));
+    }
+    return InputGeometry(g);
+}
+
+Geometry* convertToCurveIfNeeded(GEOSContextHandle_t extHandle, std::unique_ptr<Geometry> g) {
+    if (extHandle->lineToCurveParams.has_value() && g->hasCurvedComponents()) {
+        return g->getCurved(extHandle->lineToCurveParams.value()).release();
+    }
+    return g.release();
+}
+
 // CAPI_ItemVisitor is used internally by the CAPI STRtree
 // wrappers. It's defined here just to keep it out of the
 // extern "C" block.
@@ -554,6 +632,44 @@ inline void execute(GEOSContextHandle_t extHandle, F&& f) {
     }
 }
 
+template<typename F>
+Geometry* convertCurvesAndExecute(GEOSContextHandle_t extHandle, const Geometry* g1, const Geometry* g2, F&& f)
+{
+    return execute(extHandle, [&]() {
+        const auto geom1 = convertToLineIfNeeded(extHandle, g1);
+        const auto geom2 = convertToLineIfNeeded(extHandle, g2);
+
+        auto result = f(geom1.get(), geom2.get());
+
+        if (result) {
+            if (extHandle->lineToCurveParams && (geom1.isLinearized() || geom2.isLinearized())) {
+                result = result->getCurved(extHandle->lineToCurveParams.value());
+            }
+
+            result->setSRID(g1->getSRID());
+        }
+
+        return result.release();
+    });
+}
+
+template<typename F>
+Geometry* convertCurvesAndExecute(GEOSContextHandle_t extHandle, const Geometry* g1, F&& f)
+{
+    return execute(extHandle, [&]() {
+        const auto geom1 = convertToLineIfNeeded(extHandle, g1);
+
+        auto g3 = f(geom1.get());
+
+        if (extHandle->lineToCurveParams && (geom1.isLinearized())) {
+            g3 = g3->getCurved(extHandle->lineToCurveParams.value());
+        }
+
+        g3->setSRID(g1->getSRID());
+        return g3.release();
+    });
+}
+
 extern "C" {
 
     GEOSContextHandle_t
@@ -634,6 +750,24 @@ extern "C" {
         return handle->setInterruptHandler(cb, userData);
     }
 
+    void GEOSContext_setCurveToLineParams_r(GEOSContextHandle_t extHandle, const GEOSCurveToLineParams* params)
+    {
+        if (params) {
+            extHandle->curveToLineParams = *params;
+        } else {
+            extHandle->curveToLineParams = std::nullopt;
+        }
+    }
+
+    void GEOSContext_setLineToCurveParams_r(GEOSContextHandle_t extHandle, const GEOSLineToCurveParams* params)
+    {
+        if (params) {
+            extHandle->lineToCurveParams = *params;
+        } else {
+            extHandle->lineToCurveParams = std::nullopt;
+        }
+    }
+
     void
     finishGEOS_r(GEOSContextHandle_t extHandle)
     {
@@ -666,7 +800,10 @@ extern "C" {
     GEOSDisjoint_r(GEOSContextHandle_t extHandle, const Geometry* g1, const Geometry* g2)
     {
         return execute(extHandle, 2, [&]() {
-            return g1->disjoint(g2);
+            const auto geom1 = convertToLineIfNeeded(extHandle, g1);
+            const auto geom2 = convertToLineIfNeeded(extHandle, g2);
+
+            return geom1->disjoint(geom2);
         });
     }
 
@@ -674,7 +811,10 @@ extern "C" {
     GEOSTouches_r(GEOSContextHandle_t extHandle, const Geometry* g1, const Geometry* g2)
     {
         return execute(extHandle, 2, [&]() {
-            return g1->touches(g2);
+            const auto geom1 = convertToLineIfNeeded(extHandle, g1);
+            const auto geom2 = convertToLineIfNeeded(extHandle, g2);
+
+            return geom1->touches(geom2);
         });
     }
 
@@ -682,7 +822,10 @@ extern "C" {
     GEOSIntersects_r(GEOSContextHandle_t extHandle, const Geometry* g1, const Geometry* g2)
     {
         return execute(extHandle, 2, [&]() {
-            return g1->intersects(g2);
+            const auto geom1 = convertToLineIfNeeded(extHandle, g1);
+            const auto geom2 = convertToLineIfNeeded(extHandle, g2);
+
+            return geom1->intersects(geom2);
         });
     }
 
@@ -690,7 +833,10 @@ extern "C" {
     GEOSCrosses_r(GEOSContextHandle_t extHandle, const Geometry* g1, const Geometry* g2)
     {
         return execute(extHandle, 2, [&]() {
-            return g1->crosses(g2);
+            const auto geom1 = convertToLineIfNeeded(extHandle, g1);
+            const auto geom2 = convertToLineIfNeeded(extHandle, g2);
+
+            return geom1->crosses(geom2);
         });
     }
 
@@ -698,7 +844,10 @@ extern "C" {
     GEOSWithin_r(GEOSContextHandle_t extHandle, const Geometry* g1, const Geometry* g2)
     {
         return execute(extHandle, 2, [&]() {
-            return g1->within(g2);
+            const auto geom1 = convertToLineIfNeeded(extHandle, g1);
+            const auto geom2 = convertToLineIfNeeded(extHandle, g2);
+
+            return geom1->within(geom2);
         });
     }
 
@@ -706,7 +855,10 @@ extern "C" {
     GEOSContains_r(GEOSContextHandle_t extHandle, const Geometry* g1, const Geometry* g2)
     {
         return execute(extHandle, 2, [&]() {
-            return g1->contains(g2);
+            const auto geom1 = convertToLineIfNeeded(extHandle, g1);
+            const auto geom2 = convertToLineIfNeeded(extHandle, g2);
+
+            return geom1->contains(geom2);
         });
     }
 
@@ -714,7 +866,10 @@ extern "C" {
     GEOSOverlaps_r(GEOSContextHandle_t extHandle, const Geometry* g1, const Geometry* g2)
     {
         return execute(extHandle, 2, [&]() {
-            return g1->overlaps(g2);
+            const auto geom1 = convertToLineIfNeeded(extHandle, g1);
+            const auto geom2 = convertToLineIfNeeded(extHandle, g2);
+
+            return geom1->overlaps(geom2);
         });
     }
 
@@ -722,7 +877,10 @@ extern "C" {
     GEOSCovers_r(GEOSContextHandle_t extHandle, const Geometry* g1, const Geometry* g2)
     {
         return execute(extHandle, 2, [&]() {
-            return g1->covers(g2);
+            const auto geom1 = convertToLineIfNeeded(extHandle, g1);
+            const auto geom2 = convertToLineIfNeeded(extHandle, g2);
+
+            return geom1->covers(geom2);
         });
     }
 
@@ -730,7 +888,10 @@ extern "C" {
     GEOSCoveredBy_r(GEOSContextHandle_t extHandle, const Geometry* g1, const Geometry* g2)
     {
         return execute(extHandle, 2, [&]() {
-            return g1->coveredBy(g2);
+            const auto geom1 = convertToLineIfNeeded(extHandle, g1);
+            const auto geom2 = convertToLineIfNeeded(extHandle, g2);
+
+            return geom1->coveredBy(geom2);
         });
     }
 
@@ -738,7 +899,10 @@ extern "C" {
     GEOSEquals_r(GEOSContextHandle_t extHandle, const Geometry* g1, const Geometry* g2)
     {
         return execute(extHandle, 2, [&]() {
-            return g1->equals(g2);
+            const auto geom1 = convertToLineIfNeeded(extHandle, g1);
+            const auto geom2 = convertToLineIfNeeded(extHandle, g2);
+
+            return geom1->equals(geom2);
         });
     }
 
@@ -751,8 +915,11 @@ extern "C" {
     GEOSRelatePattern_r(GEOSContextHandle_t extHandle, const Geometry* g1, const Geometry* g2, const char* imPattern)
     {
         return execute(extHandle, 2, [&]() {
+            const auto geom1 = convertToLineIfNeeded(extHandle, g1);
+            const auto geom2 = convertToLineIfNeeded(extHandle, g2);
+
             std::string s(imPattern);
-            return g1->relate(g2, s);
+            return geom1->relate(geom2, s);
         });
     }
 
@@ -775,9 +942,12 @@ extern "C" {
     GEOSRelate_r(GEOSContextHandle_t extHandle, const Geometry* g1, const Geometry* g2)
     {
         return execute(extHandle, [&]() {
+            const auto geom1 = convertToLineIfNeeded(extHandle, g1);
+            const auto geom2 = convertToLineIfNeeded(extHandle, g2);
+
             using geos::geom::IntersectionMatrix;
 
-            auto im = g1->relate(g2);
+            auto im = geom1->relate(geom2);
             if(im == nullptr) {
                 return (char*) nullptr;
             }
@@ -787,7 +957,7 @@ extern "C" {
     }
 
     char*
-    GEOSRelateBoundaryNodeRule_r(GEOSContextHandle_t extHandle, const Geometry* g1, const Geometry* g2, int bnr)
+    GEOSRelateBoundaryNodeRule_r(GEOSContextHandle_t extHandle, const Geometry* geom1, const Geometry* geom2, int bnr)
     {
         using geos::operation::relate::RelateOp;
         using geos::geom::IntersectionMatrix;
@@ -795,6 +965,8 @@ extern "C" {
 
         return execute(extHandle, [&]() -> char* {
             std::unique_ptr<IntersectionMatrix> im;
+            const auto g1 = convertToLineIfNeeded(extHandle, geom1);
+            const auto g2 = convertToLineIfNeeded(extHandle, geom2);
 
             switch (bnr) {
                 case GEOSRELATE_BNR_MOD2: /* same as OGC */
@@ -842,9 +1014,11 @@ extern "C" {
         return execute(extHandle, 2, [&]() {
             GEOSContextHandleInternal_t* handle = reinterpret_cast<GEOSContextHandleInternal_t*>(extHandle);
 
+            const auto inputGeom = convertToLineIfNeeded(extHandle, g1);
+
             using geos::operation::valid::IsValidOp;
 
-            IsValidOp ivo(g1);
+            IsValidOp ivo(inputGeom);
             const TopologyValidationError* err = ivo.getValidationError();
 
             if(err) {
@@ -861,12 +1035,14 @@ extern "C" {
     GEOSisValidReason_r(GEOSContextHandle_t extHandle, const Geometry* g1)
     {
         return execute(extHandle, [&]() {
+            const auto inputGeom = convertToLineIfNeeded(extHandle, g1);
+
             using geos::operation::valid::IsValidOp;
 
             char* result = nullptr;
             char const* const validstr = "Valid Geometry";
 
-            IsValidOp ivo(g1);
+            IsValidOp ivo(inputGeom);
             const TopologyValidationError* err = ivo.getValidationError();
 
             if(err) {
@@ -893,7 +1069,9 @@ extern "C" {
         using geos::operation::valid::IsValidOp;
 
         return execute(extHandle, 2, [&]() {
-            IsValidOp ivo(g);
+            const auto inputGeom = convertToLineIfNeeded(extHandle, g);
+
+            IsValidOp ivo(inputGeom);
             if(flags & GEOSVALID_ALLOW_SELFTOUCHING_RING_FORMING_HOLE) {
                 ivo.setSelfTouchingRingFormingHoleValid(true);
             }
@@ -961,7 +1139,10 @@ extern "C" {
     GEOSDistanceIndexed_r(GEOSContextHandle_t extHandle, const Geometry* g1, const Geometry* g2, double* dist)
     {
         return execute(extHandle, 0, [&]() {
-            *dist = IndexedFacetDistance::distance(g1, g2);
+            const auto input1 = convertToLineIfNeeded(extHandle, g1);
+            const auto input2 = convertToLineIfNeeded(extHandle, g2);
+
+            *dist = IndexedFacetDistance::distance(input1, input2);
             return 1;
         });
     }
@@ -970,7 +1151,10 @@ extern "C" {
     GEOSHausdorffDistance_r(GEOSContextHandle_t extHandle, const Geometry* g1, const Geometry* g2, double* dist)
     {
         return execute(extHandle, 0, [&]() {
-            *dist = DiscreteHausdorffDistance::distance(*g1, *g2);
+            const auto input1 = convertToLineIfNeeded(extHandle, g1);
+            const auto input2 = convertToLineIfNeeded(extHandle, g2);
+
+            *dist = DiscreteHausdorffDistance::distance(*input1, *input2);
             return 1;
         });
     }
@@ -982,7 +1166,10 @@ extern "C" {
     	double* p2x, double* p2y)
     {
         return execute(extHandle, 0, [&]() {
-            DiscreteHausdorffDistance dhd(*g1, *g2);
+            const auto input1 = convertToLineIfNeeded(extHandle, g1);
+            const auto input2 = convertToLineIfNeeded(extHandle, g2);
+
+            DiscreteHausdorffDistance dhd(*input1, *input2);
             *dist = dhd.distance();
             const auto& pts = dhd.getCoordinates();
             *p1x = pts[0].x;
@@ -998,7 +1185,10 @@ extern "C" {
                                    double densifyFrac, double* dist)
     {
         return execute(extHandle, 0, [&]() {
-            *dist = DiscreteHausdorffDistance::distance(*g1, *g2, densifyFrac);
+            const auto input1 = convertToLineIfNeeded(extHandle, g1);
+            const auto input2 = convertToLineIfNeeded(extHandle, g2);
+
+            *dist = DiscreteHausdorffDistance::distance(*input1, *input2, densifyFrac);
             return 1;
         });
     }
@@ -1008,7 +1198,10 @@ extern "C" {
 		                   double densifyFrac, double* dist, double* p1x, double* p1y, double* p2x, double* p2y)
     {
         return execute(extHandle, 0, [&]() {
-            DiscreteHausdorffDistance dhd(*g1, *g2);
+            const auto input1 = convertToLineIfNeeded(extHandle, g1);
+            const auto input2 = convertToLineIfNeeded(extHandle, g2);
+
+            DiscreteHausdorffDistance dhd(*input1, *input2);
             dhd.setDensifyFraction(densifyFrac);
             *dist = dhd.distance();
             const auto& pts = dhd.getCoordinates();
@@ -1024,7 +1217,10 @@ extern "C" {
     GEOSFrechetDistance_r(GEOSContextHandle_t extHandle, const Geometry* g1, const Geometry* g2, double* dist)
     {
         return execute(extHandle, 0, [&]() {
-            *dist = DiscreteFrechetDistance::distance(*g1, *g2);
+            const auto input1 = convertToLineIfNeeded(extHandle, g1);
+            const auto input2 = convertToLineIfNeeded(extHandle, g2);
+
+            *dist = DiscreteFrechetDistance::distance(*input1, *input2);
             return 1;
         });
     }
@@ -1034,7 +1230,10 @@ extern "C" {
                                  double* dist)
     {
         return execute(extHandle, 0, [&]() {
-            *dist = DiscreteFrechetDistance::distance(*g1, *g2, densifyFrac);
+            const auto input1 = convertToLineIfNeeded(extHandle, g1);
+            const auto input2 = convertToLineIfNeeded(extHandle, g2);
+
+            *dist = DiscreteFrechetDistance::distance(*input1, *input2, densifyFrac);
             return 1;
         });
     }
@@ -1064,7 +1263,11 @@ extern "C" {
             if(g1->isEmpty() || g2->isEmpty()) {
                 return nullptr;
             }
-            return geos::operation::distance::DistanceOp::nearestPoints(g1, g2).release();
+
+            const auto input1 = convertToLineIfNeeded(extHandle, g1);
+            const auto input2 = convertToLineIfNeeded(extHandle, g2);
+
+            return geos::operation::distance::DistanceOp::nearestPoints(input1, input2).release();
         });
     }
 
@@ -1084,8 +1287,8 @@ extern "C" {
     {
         return execute(extHandle, [&]() {
             geos::operation::cluster::DBSCANClusterFinder finder(eps, minPoints);
-
-            return capi_clusters(g, finder);
+            const auto input = convertToLineIfNeeded(extHandle, g);
+            return capi_clusters(input, finder);
         });
     }
 
@@ -1094,7 +1297,8 @@ extern "C" {
     {
         return execute(extHandle, [&]() {
             geos::operation::cluster::GeometryIntersectsClusterFinder finder;
-            return capi_clusters(g, finder);
+            const auto input = convertToLineIfNeeded(extHandle, g);
+            return capi_clusters(input, finder);
         });
     }
 
@@ -1121,7 +1325,8 @@ extern "C" {
     {
         return execute(extHandle, [&]() {
             geos::operation::cluster::GeometryDistanceClusterFinder finder(d);
-            return capi_clusters(g, finder);
+            const auto input = convertToLineIfNeeded(extHandle, g);
+            return capi_clusters(input, finder);
         });
     }
 
@@ -1282,7 +1487,7 @@ extern "C" {
     GEOSisSimple_r(GEOSContextHandle_t extHandle, const Geometry* g1)
     {
         return execute(extHandle, 2, [&]() {
-            return g1->isSimple();
+            return convertToLineIfNeeded(extHandle, g1)->isSimple();
         });
     }
 
@@ -1290,7 +1495,9 @@ extern "C" {
     GEOSisSimpleDetail_r(GEOSContextHandle_t extHandle, const Geometry* g1, int returnAllPoints, Geometry** result)
     {
         return execute(extHandle, 2, [&]() {
-            geos::operation::valid::IsSimpleOp iso(g1);
+            const auto inputGeom = convertToLineIfNeeded(extHandle, g1);
+
+            geos::operation::valid::IsSimpleOp iso(inputGeom);
             iso.setFindAllLocations(returnAllPoints);
 
             *result = nullptr;
@@ -1313,7 +1520,8 @@ extern "C" {
     GEOSisRing_r(GEOSContextHandle_t extHandle, const Geometry* g)
     {
         return execute(extHandle, 2, [&]() {
-            const Curve* ls = dynamic_cast<const Curve*>(g);
+            const auto inputGeom = convertToLineIfNeeded(extHandle, g);
+            const Curve* ls = dynamic_cast<const Curve*>(inputGeom.get());
             if(ls) {
                 return ls->isRing();
             }
@@ -1361,10 +1569,8 @@ extern "C" {
     Geometry*
     GEOSIntersection_r(GEOSContextHandle_t extHandle, const Geometry* g1, const Geometry* g2)
     {
-        return execute(extHandle, [&]() {
-            auto g3 = g1->intersection(g2);
-            g3->setSRID(g1->getSRID());
-            return g3.release();
+        return convertCurvesAndExecute(extHandle, g1, g2, [](const Geometry* geom1, const Geometry* geom2) {
+            return geom1->intersection(geom2);
         });
     }
 
@@ -1392,7 +1598,9 @@ extern "C" {
     GEOSBuffer_r(GEOSContextHandle_t extHandle, const Geometry* g1, double width, int quadrantsegments)
     {
         return execute(extHandle, [&]() {
-            auto g3 = g1->buffer(width, quadrantsegments);
+            const auto inputGeom = convertToLineIfNeeded(extHandle, g1);
+
+            auto g3 = inputGeom->buffer(width, quadrantsegments);
             g3->setSRID(g1->getSRID());
             return g3.release();
         });
@@ -1407,6 +1615,8 @@ extern "C" {
         using geos::util::IllegalArgumentException;
 
         return execute(extHandle, [&]() {
+            const auto inputGeom = convertToLineIfNeeded(extHandle, g1);
+
             BufferParameters bp;
             bp.setQuadrantSegments(quadsegs);
 
@@ -1424,7 +1634,7 @@ extern "C" {
                 static_cast<BufferParameters::JoinStyle>(joinStyle)
             );
             bp.setMitreLimit(mitreLimit);
-            BufferOp op(g1, bp);
+            BufferOp op(inputGeom, bp);
             std::unique_ptr<Geometry> g3 = op.getResultGeometry(width);
             g3->setSRID(g1->getSRID());
             return g3.release();
@@ -1437,7 +1647,9 @@ extern "C" {
         using geos::geom::util::Densifier;
 
         return execute(extHandle, [&]() {
-            Densifier densifier(g);
+            const auto inputGeom = convertToLineIfNeeded(extHandle, g);
+
+            Densifier densifier(inputGeom);
             densifier.setDistanceTolerance(tolerance);
             auto g3 = densifier.getResultGeometry();
             g3->setSRID(g->getSRID());
@@ -1450,6 +1662,8 @@ extern "C" {
                       double mitreLimit)
     {
         return execute(extHandle, [&]() {
+            const auto inputGeom = convertToLineIfNeeded(extHandle, g1);
+
             BufferParameters bp;
             //-- use default cap style ROUND
             bp.setQuadrantSegments(quadsegs);
@@ -1462,7 +1676,7 @@ extern "C" {
             );
             bp.setMitreLimit(mitreLimit);
 
-            OffsetCurve oc(*g1, width, bp);
+            OffsetCurve oc(*inputGeom, width, bp);
             std::unique_ptr<Geometry> g3 = oc.getCurve();
             g3->setSRID(g1->getSRID());
             return g3.release();
@@ -1499,7 +1713,8 @@ extern "C" {
     GEOSConvexHull_r(GEOSContextHandle_t extHandle, const Geometry* g1)
     {
         return execute(extHandle, [&]() {
-            auto g3 = g1->convexHull();
+            const auto inputGeom = convertToLineIfNeeded(extHandle, g1);
+            auto g3 = inputGeom->convexHull();
             g3->setSRID(g1->getSRID());
             return g3.release();
         });
@@ -1512,7 +1727,9 @@ extern "C" {
         unsigned int allowHoles)
     {
         return execute(extHandle, [&]() {
-            ConcaveHull hull(g1);
+            const auto inputGeom = convertToLineIfNeeded(extHandle, g1);
+
+            ConcaveHull hull(inputGeom);
             hull.setMaximumEdgeLengthRatio(ratio);
             hull.setHolesAllowed(allowHoles);
             std::unique_ptr<Geometry> g3 = hull.getHull();
@@ -1528,7 +1745,9 @@ extern "C" {
         unsigned int allowHoles)
     {
         return execute(extHandle, [&]() {
-            ConcaveHull hull(g1);
+            const auto inputGeom = convertToLineIfNeeded(extHandle, g1);
+
+            ConcaveHull hull(inputGeom);
             hull.setMaximumEdgeLength(length);
             hull.setHolesAllowed(allowHoles);
             std::unique_ptr<Geometry> g3 = hull.getHull();
@@ -1543,10 +1762,8 @@ extern "C" {
         unsigned int isOuter,
         double vertexNumFraction)
     {
-        return execute(extHandle, [&]() {
-            std::unique_ptr<Geometry> g3 = PolygonHullSimplifier::hull(g1, isOuter, vertexNumFraction);
-            g3->setSRID(g1->getSRID());
-            return g3.release();
+        return convertCurvesAndExecute(extHandle, g1, [&](const GEOSGeometry* inputGeom) {
+            return PolygonHullSimplifier::hull(inputGeom, isOuter, vertexNumFraction);
         });
     }
 
@@ -1557,16 +1774,12 @@ extern "C" {
         unsigned int parameterMode,
         double parameter)
     {
-        return execute(extHandle, [&]() {
+        return convertCurvesAndExecute(extHandle, g1, [&](const Geometry* inputGeom) {
             if (parameterMode == GEOSHULL_PARAM_AREA_RATIO) {
-                std::unique_ptr<Geometry> g3 = PolygonHullSimplifier::hullByAreaDelta(g1, isOuter, parameter);
-                g3->setSRID(g1->getSRID());
-                return g3.release();
+                return PolygonHullSimplifier::hullByAreaDelta(inputGeom, isOuter, parameter);
             }
             else if (parameterMode == GEOSHULL_PARAM_VERTEX_RATIO) {
-                std::unique_ptr<Geometry> g3 = PolygonHullSimplifier::hull(g1, isOuter, parameter);
-                g3->setSRID(g1->getSRID());
-                return g3.release();
+                return PolygonHullSimplifier::hull(inputGeom, isOuter, parameter);
             }
             else {
                 throw IllegalArgumentException("GEOSPolygonHullSimplifyMode_r: Unknown parameterMode");
@@ -1582,9 +1795,11 @@ extern "C" {
         unsigned int isHolesAllowed)
     {
         return execute(extHandle, [&]() {
+            const auto inputGeom = convertToLineIfNeeded(extHandle, g1);
+
             std::unique_ptr<Geometry> g3 =
                 ConcaveHullOfPolygons::concaveHullByLengthRatio(
-                    g1, lengthRatio,
+                    inputGeom, lengthRatio,
                     isTight > 0,
                     isHolesAllowed > 0);
             g3->setSRID(g1->getSRID());
@@ -1598,7 +1813,9 @@ extern "C" {
         using geos::algorithm::MinimumAreaRectangle;
 
         return execute(extHandle, [&]() {
-            auto g3 = MinimumAreaRectangle::getMinimumRectangle(g);
+            const auto inputGeom = convertToLineIfNeeded(extHandle, g);
+
+            auto g3 = MinimumAreaRectangle::getMinimumRectangle(inputGeom);
             g3->setSRID(g->getSRID());
             return g3.release();
         });
@@ -1608,7 +1825,9 @@ extern "C" {
     GEOSMaximumInscribedCircle_r(GEOSContextHandle_t extHandle, const Geometry* g, double tolerance)
     {
         return execute(extHandle, [&]() {
-            geos::algorithm::construct::MaximumInscribedCircle mic(g, tolerance);
+            const auto inputGeom = convertToLineIfNeeded(extHandle, g);
+
+            geos::algorithm::construct::MaximumInscribedCircle mic(inputGeom, tolerance);
             auto g3 = mic.getRadiusLine();
             g3->setSRID(g->getSRID());
             return g3.release();
@@ -1619,7 +1838,10 @@ extern "C" {
     GEOSLargestEmptyCircle_r(GEOSContextHandle_t extHandle, const Geometry* g, const GEOSGeometry* boundary, double tolerance)
     {
         return execute(extHandle, [&]() {
-            geos::algorithm::construct::LargestEmptyCircle lec(g, boundary, tolerance);
+            const auto obstacles = convertToLineIfNeeded(extHandle, g);
+            const auto bounds = convertToLineIfNeeded(extHandle, boundary);
+
+            geos::algorithm::construct::LargestEmptyCircle lec(obstacles, bounds, tolerance);
             auto g3 = lec.getRadiusLine();
             g3->setSRID(g->getSRID());
             return g3.release();
@@ -1630,7 +1852,9 @@ extern "C" {
     GEOSMinimumWidth_r(GEOSContextHandle_t extHandle, const Geometry* g)
     {
         return execute(extHandle, [&]() {
-            geos::algorithm::MinimumDiameter m(g);
+            const auto inputGeom = convertToLineIfNeeded(extHandle, g);
+
+            geos::algorithm::MinimumDiameter m(inputGeom);
             auto g3 = m.getDiameter();
             g3->setSRID(g->getSRID());
             return g3.release();
@@ -1641,7 +1865,8 @@ extern "C" {
     GEOSMinimumClearanceLine_r(GEOSContextHandle_t extHandle, const Geometry* g)
     {
         return execute(extHandle, [&]() {
-            geos::precision::MinimumClearance mc(g);
+            const auto inputGeom = convertToLineIfNeeded(extHandle, g);
+            geos::precision::MinimumClearance mc(inputGeom);
             auto g3 = mc.getLine();
             g3->setSRID(g->getSRID());
             return g3.release();
@@ -1652,7 +1877,8 @@ extern "C" {
     GEOSMinimumClearance_r(GEOSContextHandle_t extHandle, const Geometry* g, double* d)
     {
         return execute(extHandle, 2, [&]() {
-            geos::precision::MinimumClearance mc(g);
+            const auto inputGeom = convertToLineIfNeeded(extHandle, g);
+            geos::precision::MinimumClearance mc(inputGeom);
             double res = mc.getDistance();
             *d = res;
             return 0;
@@ -1735,10 +1961,8 @@ extern "C" {
     Geometry*
     GEOSUnion_r(GEOSContextHandle_t extHandle, const Geometry* g1, const Geometry* g2)
     {
-        return execute(extHandle, [&]() {
-            auto g3 = g1->Union(g2);
-            g3->setSRID(g1->getSRID());
-            return g3.release();
+        return convertCurvesAndExecute(extHandle, g1, g2, [&](const Geometry* input1, const Geometry* input2) {
+            return input1->Union(input2);
         });
     }
 
@@ -1766,30 +1990,24 @@ extern "C" {
     Geometry*
     GEOSCoverageUnion_r(GEOSContextHandle_t extHandle, const Geometry* g)
     {
-        return execute(extHandle, [&]() {
-            auto g3 = geos::coverage::CoverageUnion::Union(g);
-            g3->setSRID(g->getSRID());
-            return g3.release();
+        return convertCurvesAndExecute(extHandle, g, [&](const Geometry* inputGeom) {
+            return geos::coverage::CoverageUnion::Union(inputGeom);
         });
     }
 
     Geometry*
     GEOSDisjointSubsetUnion_r(GEOSContextHandle_t extHandle, const Geometry* g)
     {
-        return execute(extHandle, [&]() {
-            auto g3 = geos::operation::geounion::DisjointSubsetUnion::Union(g);
-            g3->setSRID(g->getSRID());
-            return g3.release();
+        return convertCurvesAndExecute(extHandle, g, [&](const Geometry* inputGeom) {
+            return geos::operation::geounion::DisjointSubsetUnion::Union(inputGeom);
         });
     }
 
     Geometry*
     GEOSUnaryUnion_r(GEOSContextHandle_t extHandle, const Geometry* g)
     {
-        return execute(extHandle, [&]() {
-            std::unique_ptr<Geometry> g3(g->Union());
-            g3->setSRID(g->getSRID());
-            return g3.release();
+        return convertCurvesAndExecute(extHandle, g, [&](const Geometry* input1) {
+            return input1->Union();
         });
     }
 
@@ -1853,7 +2071,7 @@ extern "C" {
     GEOSPointOnSurface_r(GEOSContextHandle_t extHandle, const Geometry* g1)
     {
         return execute(extHandle, [&]() {
-            auto ret = g1->getInteriorPoint();
+            auto ret = convertToLineIfNeeded(extHandle, g1)->getInteriorPoint();
             ret->setSRID(g1->getSRID());
             return ret.release();
         });
@@ -1862,13 +2080,13 @@ extern "C" {
     Geometry*
     GEOSClipByRect_r(GEOSContextHandle_t extHandle, const Geometry* g, double xmin, double ymin, double xmax, double ymax)
     {
-        return execute(extHandle, [&]() {
+        return convertCurvesAndExecute(extHandle, g, [&](const Geometry* inputGeom) {
             using geos::operation::intersection::Rectangle;
             using geos::operation::intersection::RectangleIntersection;
             Rectangle rect(xmin, ymin, xmax, ymax);
-            std::unique_ptr<Geometry> g3 = RectangleIntersection::clip(*g, rect);
+            std::unique_ptr<Geometry> g3 = RectangleIntersection::clip(*inputGeom, rect);
             g3->setSRID(g->getSRID());
-            return g3.release();
+            return g3;
         });
     }
 
@@ -1876,13 +2094,13 @@ extern "C" {
     GEOSSubdivideByGrid_r(GEOSContextHandle_t extHandle, const Geometry* g, double xmin, double ymin,
                           double xmax, double ymax, unsigned nx, unsigned ny, int include_exterior)
     {
-        return execute(extHandle, [&]() {
+        return convertCurvesAndExecute(extHandle, g, [&](const Geometry* inputGeom) {
             Envelope env(xmin, xmax, ymin, ymax);
             double dx = env.getWidth() / static_cast<double>(nx);
             double dy = env.getHeight() / static_cast<double>(ny);
             geos::operation::grid::Grid<geos::operation::grid::bounded_extent> grid(env, dx, dy);
 
-            return geos::operation::grid::GridIntersection::subdividePolygon(grid, *g, include_exterior).release();
+            return geos::operation::grid::GridIntersection::subdividePolygon(grid, *inputGeom, include_exterior);
         });
     }
 
@@ -1895,6 +2113,8 @@ extern "C" {
 #pragma GCC diagnostic ignored "-Warray-bounds"
 #endif
         return execute(extHandle, 0, [&]() {
+            const auto inputGeom = convertToLineIfNeeded(extHandle, g);
+
             Envelope env(xmin, xmax, ymin, ymax);
             double dx = env.getWidth() / static_cast<double>(nx);
             double dy = env.getHeight() / static_cast<double>(ny);
@@ -1905,7 +2125,7 @@ extern "C" {
             std::shared_ptr<float[]> bufPtr(buf, [](float*) {});
 
             auto cov = std::make_shared<geos::operation::grid::Matrix<float>>(ny, nx, bufPtr);
-            geos::operation::grid::GridIntersection isect(grid, *g, cov);
+            geos::operation::grid::GridIntersection isect(grid, *inputGeom, cov);
 
             return 1;
         });
@@ -2310,7 +2530,9 @@ extern "C" {
     GEOSGetCentroid_r(GEOSContextHandle_t extHandle, const Geometry* g)
     {
         return execute(extHandle, [&]() -> Geometry* {
-            auto ret = g->getCentroid();
+            const auto inputGeom = convertToLineIfNeeded(extHandle, g);
+
+            auto ret = inputGeom->getCentroid();
             ret->setSRID(g->getSRID());
             return ret.release();
         });
@@ -2337,8 +2559,9 @@ extern "C" {
     {
         return execute(extHandle, [&]() -> Geometry* {
             GEOSContextHandleInternal_t* handle = reinterpret_cast<GEOSContextHandleInternal_t*>(extHandle);
+            const auto inputGeom = convertToLineIfNeeded(extHandle, g);
 
-            geos::algorithm::MinimumBoundingCircle mc(g);
+            geos::algorithm::MinimumBoundingCircle mc(inputGeom);
             std::unique_ptr<Geometry> ret = mc.getCircle();
             const GeometryFactory* gf = handle->geomFactory;
             if (center) *center = gf->createPoint(mc.getCentre()).release();
@@ -2595,33 +2818,25 @@ extern "C" {
     Geometry*
     GEOSMakeValidWithParams_r(
         GEOSContextHandle_t extHandle,
-        const Geometry* g,
+        const Geometry* input,
         const GEOSMakeValidParams* params)
     {
         using geos::geom::util::GeometryFixer;
         using geos::operation::valid::MakeValid;
 
-        if (params && params->method == GEOS_MAKE_VALID_LINEWORK) {
-            return execute(extHandle, [&]() {
+        return convertCurvesAndExecute(extHandle, input, [params](const Geometry* g) {
+            if (params && params->method == GEOS_MAKE_VALID_LINEWORK) {
                 MakeValid makeValid;
-                auto out = makeValid.build(g);
-                out->setSRID(g->getSRID());
-                return out.release();
-            });
-        }
-        else if (params && params->method == GEOS_MAKE_VALID_STRUCTURE) {
-            return execute(extHandle, [&]() {
+                return makeValid.build(g);
+            }
+            if (params && params->method == GEOS_MAKE_VALID_STRUCTURE) {
                 GeometryFixer fixer(g);
                 fixer.setKeepCollapsed(params->keepCollapsed == 0 ? false : true);
-                auto out = fixer.getResult();
-                out->setSRID(g->getSRID());
-                return out.release();
-            });
-        }
-        else {
-            extHandle->ERROR_MESSAGE("Unknown method in GEOSMakeValidParams");
-            return nullptr;
-        }
+                return fixer.getResult();
+            }
+
+            throw IllegalArgumentException("Unknown method in GEOSMakeValidParams");
+        });
     }
 
     Geometry*
@@ -4430,7 +4645,9 @@ extern "C" {
                 throw std::runtime_error("third argument of GEOSProject_r must be Point");
             }
             const geos::geom::Coordinate inputPt(*p->getCoordinate());
-            return geos::linearref::LengthIndexedLine(g).project(inputPt);
+            const auto inputLine = convertToLineIfNeeded(extHandle, g);
+
+            return geos::linearref::LengthIndexedLine(inputLine).project(inputPt);
         });
     }
 
@@ -4441,8 +4658,9 @@ extern "C" {
         return execute(extHandle, [&]() {
             GEOSContextHandleInternal_t* handle = reinterpret_cast<GEOSContextHandleInternal_t*>(extHandle);
             const GeometryFactory* gf = handle->geomFactory;
+            const auto inputGeom = convertToLineIfNeeded(extHandle, g);
 
-            geos::linearref::LengthIndexedLine lil(g);
+            geos::linearref::LengthIndexedLine lil(inputGeom);
             CoordinateXYZM coord = lil.extractPoint(d);
 
             std::unique_ptr<Point> point;
@@ -4465,24 +4683,22 @@ extern "C" {
     GEOSProjectNormalized_r(GEOSContextHandle_t extHandle, const Geometry* g,
                             const Geometry* p)
     {
+        return execute(extHandle, -1, [&]() {
+            const auto inputLine = convertToLineIfNeeded(extHandle, g);
 
-        double length;
-        double distance;
-        if(GEOSLength_r(extHandle, g, &length) != 1) {
-            return -1.0;
-        };
+            double length = inputLine->getLength();
+            double distance = GEOSProject_r(extHandle, inputLine.get(), p);
 
-        distance = GEOSProject_r(extHandle, g, p);
+            if (distance == 0.0 && length == 0.0)
+                return 0.0;
 
-        if (distance == 0.0 && length == 0.0)
-            return 0.0;
-
-        /* Meaningless projection? error */
-        if (distance < 0.0 || ! std::isfinite(distance) || length == 0.0) {
-            return -1.0;
-        } else {
-            return distance / length;
-        }
+            /* Meaningless projection? error */
+            if (distance < 0.0 || ! std::isfinite(distance) || length == 0.0) {
+                return -1.0;
+            } else {
+                return distance / length;
+            }
+        });
     }
 
 
@@ -4553,7 +4769,12 @@ extern "C" {
 
         SharedPathsOp::PathList forw, back;
         try {
-            SharedPathsOp::sharedPathsOp(*g1, *g2, forw, back);
+            const auto inputGeom1 = convertToLineIfNeeded(handle, g1);
+            const auto inputGeom2 = convertToLineIfNeeded(handle, g2);
+
+            SharedPathsOp::sharedPathsOp(*inputGeom1, *inputGeom2, forw, back);
+            // Result will be a MultiLineString of line segments.
+            // Geometry::getCurved() will pass it through unmodified.
         }
         catch(const std::exception& e) {
             SharedPathsOp::clearEdges(forw);
@@ -4697,7 +4918,9 @@ extern "C" {
         using geos::operation::buffer::BufferOp;
 
         return execute(extHandle, [&]() {
-            BufferOp op(g1, *bp);
+            const auto inputGeom = convertToLineIfNeeded(extHandle, g1);
+
+            BufferOp op(inputGeom, *bp);
             std::unique_ptr<Geometry> g3 = op.getResultGeometry(width);
             g3->setSRID(g1->getSRID());
             return g3.release();
@@ -4733,7 +4956,9 @@ extern "C" {
         using geos::triangulate::polygon::ConstrainedDelaunayTriangulator;
 
         return execute(extHandle, [&]() -> Geometry* {
-            return ConstrainedDelaunayTriangulator::triangulate(g1).release();
+            const auto inputGeom = convertToLineIfNeeded(extHandle, g1);
+
+            return ConstrainedDelaunayTriangulator::triangulate(inputGeom).release();
         });
     }
 
@@ -4795,7 +5020,9 @@ extern "C" {
         using geos::coverage::CoverageValidator;
 
         return execute(extHandle, 2, [&]() {
-            const GeometryCollection* col = dynamic_cast<const GeometryCollection*>(input);
+            const auto linearized = convertToLineIfNeeded(extHandle, input);
+
+            const GeometryCollection* col = dynamic_cast<const GeometryCollection*>(linearized.get());
             if (!col)
                 throw geos::util::IllegalArgumentException("input is not a collection");
 
@@ -4837,8 +5064,8 @@ extern "C" {
     {
         using geos::coverage::CoverageSimplifier;
 
-        return execute(extHandle, [&]() -> Geometry* {
-            const GeometryCollection* col = dynamic_cast<const GeometryCollection*>(input);
+        return convertCurvesAndExecute(extHandle, input, [&](const Geometry* linearized) -> std::unique_ptr<Geometry> {
+            const GeometryCollection* col = dynamic_cast<const GeometryCollection*>(linearized);
             if (!col)
                 return nullptr;
 
@@ -4857,8 +5084,7 @@ extern "C" {
             else return nullptr;
 
             const GeometryFactory* gf = input->getFactory();
-            std::unique_ptr<Geometry> r = gf->createGeometryCollection(std::move(simple));
-            return r.release();
+            return gf->createGeometryCollection(std::move(simple));
         });
     }
 
@@ -4932,8 +5158,8 @@ extern "C" {
     {
         using geos::coverage::CoverageCleaner;
 
-        return execute(extHandle, [&]() -> Geometry* {
-            const GeometryCollection* col = dynamic_cast<const GeometryCollection*>(input);
+        return convertCurvesAndExecute(extHandle, input, [params](const Geometry* linearized) -> std::unique_ptr<Geometry> {
+            const GeometryCollection* col = dynamic_cast<const GeometryCollection*>(linearized);
             if (!col)
                 return nullptr;
 
@@ -4950,9 +5176,8 @@ extern "C" {
             c.clean();
 
             auto cleanCov = c.getResult();
-            const GeometryFactory* gf = input->getFactory();
-            std::unique_ptr<Geometry> r = gf->createGeometryCollection(std::move(cleanCov));
-            return r.release();
+            const GeometryFactory* gf = linearized->getFactory();
+            return gf->createGeometryCollection(std::move(cleanCov));
         });
     }
 
diff --git a/src/coverage/CoverageCleaner.cpp b/src/coverage/CoverageCleaner.cpp
index 51c822e05..8df6aacf2 100644
--- a/src/coverage/CoverageCleaner.cpp
+++ b/src/coverage/CoverageCleaner.cpp
@@ -106,7 +106,11 @@ CoverageCleaner::CoverageCleaner(std::vector<const Geometry*>& p_coverage)
     : coverage(p_coverage)
     , geomFactory(p_coverage.empty() ? nullptr : coverage[0]->getFactory())
     , snappingDistance(computeDefaultSnappingDistance(p_coverage))
-{}
+{
+    for (const auto& geom : coverage) {
+        util::ensureNoCurvedComponents(geom);
+    }
+}
 
 
 /* public */
diff --git a/src/triangulate/DelaunayTriangulationBuilder.cpp b/src/triangulate/DelaunayTriangulationBuilder.cpp
index cd80ecdcd..424de2a13 100644
--- a/src/triangulate/DelaunayTriangulationBuilder.cpp
+++ b/src/triangulate/DelaunayTriangulationBuilder.cpp
@@ -77,8 +77,6 @@ DelaunayTriangulationBuilder::DelaunayTriangulationBuilder() :
 void
 DelaunayTriangulationBuilder::setSites(const Geometry& geom)
 {
-    util::ensureNoCurvedComponents(geom);
-
     // remove any duplicate points (they will cause the triangulation to fail)
     siteCoords = extractUniqueCoordinates(geom);
 }
diff --git a/src/triangulate/VoronoiDiagramBuilder.cpp b/src/triangulate/VoronoiDiagramBuilder.cpp
index eeae7f064..d4b09c8c5 100644
--- a/src/triangulate/VoronoiDiagramBuilder.cpp
+++ b/src/triangulate/VoronoiDiagramBuilder.cpp
@@ -51,7 +51,6 @@ VoronoiDiagramBuilder::VoronoiDiagramBuilder() :
 void
 VoronoiDiagramBuilder::setSites(const geom::Geometry& geom)
 {
-    util::ensureNoCurvedComponents(geom);
     siteCoords = DelaunayTriangulationBuilder::extractUniqueCoordinates(geom);
     inputGeom = &geom;
 }
diff --git a/tests/unit/capi/GEOSAreaTest.cpp b/tests/unit/capi/GEOSAreaTest.cpp
new file mode 100644
index 000000000..2c428004c
--- /dev/null
+++ b/tests/unit/capi/GEOSAreaTest.cpp
@@ -0,0 +1,35 @@
+#include <tut/tut.hpp>
+// geos
+#include <geos_c.h>
+#include <geos/constants.h>
+
+#include "capi_test_utils.h"
+
+namespace tut {
+    //
+    // Test Group
+    //
+
+    struct test_geosarea_data : public capitest::utility {};
+
+    typedef test_group<test_geosarea_data> group;
+    typedef group::object object;
+
+    group test_geosarea("capi::GEOSArea");
+
+    template<>
+    template<>
+    void object::test<1>()
+    {
+        set_test_name("curved inputs");
+
+        input_ = fromWKT("CURVEPOLYGON(COMPOUNDCURVE(CIRCULARSTRING (0 0, 1 1, 2 0), (2 0, 0 0)))");
+        ensure(input_ != nullptr);
+
+        double area = -1;
+        int ret = GEOSArea(input_, &area);
+        ensure_equals(ret, 1);
+        ensure_equals(area, geos::MATH_PI / 2);
+    }
+
+} // namespace tut
diff --git a/tests/unit/capi/GEOSBufferTest.cpp b/tests/unit/capi/GEOSBufferTest.cpp
index f8a555bf3..458b01ce9 100644
--- a/tests/unit/capi/GEOSBufferTest.cpp
+++ b/tests/unit/capi/GEOSBufferTest.cpp
@@ -23,9 +23,13 @@ struct test_capigeosbuffer_data : public capitest::utility
         : bp_(nullptr)
     {}
 
-    ~test_capigeosbuffer_data()
+    ~test_capigeosbuffer_data() override
     {
-        GEOSBufferParams_destroy(bp_);
+        if (ctxt_) {
+            GEOSBufferParams_destroy_r(ctxt_, bp_);
+        } else {
+            GEOSBufferParams_destroy(bp_);
+        }
     }
 
 };
@@ -465,11 +469,44 @@ template<>
 template<>
 void object::test<26>()
 {
+    set_test_name("curved input");
+    useContext();
+
+    bp_ = GEOSBufferParams_create_r(ctxt_);
+
     input_ = fromWKT("CIRCULARSTRING (0 0, 1 1, 2 0)");
     ensure(input_ != nullptr);
 
-    result_ = GEOSBuffer(input_, 1, 8);
+    result_ = GEOSBuffer_r(ctxt_, input_, 1, 8);
     ensure(result_ == nullptr);
+
+    result_ = GEOSBufferWithStyle_r(ctxt_, input_, 1, 8,
+        GEOSBUF_CAP_ROUND,
+        GEOSBUF_JOIN_BEVEL,
+        5.0);
+    ensure(result_ == nullptr);
+
+    result_ = GEOSBufferWithParams_r(ctxt_, input_, bp_, 1);
+    ensure(result_ == nullptr);
+
+    useCurveConversion();
+
+    result_ = GEOSBuffer_r(ctxt_, input_, 1, 8);
+    ensure(result_ != nullptr);
+    ensure_equals(GEOSGeomTypeId_r(ctxt_, result_), GEOS_POLYGON);
+    GEOSGeom_destroy_r(ctxt_, result_);
+
+    result_ = GEOSBufferWithStyle_r(ctxt_, input_, 1, 8,
+        GEOSBUF_CAP_ROUND,
+        GEOSBUF_JOIN_BEVEL,
+        5.0);
+    ensure(result_ != nullptr);
+    ensure_equals(GEOSGeomTypeId_r(ctxt_, result_), GEOS_POLYGON);
+    GEOSGeom_destroy_r(ctxt_, result_);
+
+    result_ = GEOSBufferWithParams_r(ctxt_, input_, bp_, 1);
+    ensure(result_ != nullptr);
+    ensure_equals(GEOSGeomTypeId_r(ctxt_, result_), GEOS_POLYGON);
 }
 
 template<>
diff --git a/tests/unit/capi/GEOSClipByRectTest.cpp b/tests/unit/capi/GEOSClipByRectTest.cpp
index c1c77aace..31eacd2f4 100644
--- a/tests/unit/capi/GEOSClipByRectTest.cpp
+++ b/tests/unit/capi/GEOSClipByRectTest.cpp
@@ -294,11 +294,28 @@ template<>
 template<>
 void object::test<17>()
 {
+    set_test_name("curved inputs");
+    useContext();
+
     input_ = fromWKT("CIRCULARSTRING (0 0, 1 1, 2 0)");
     ensure(input_ != nullptr);
 
-    result_ = GEOSClipByRect(input_, 0, 0, 1, 1);
+    result_ = GEOSClipByRect_r(ctxt_, input_, 0, 0, 1, 1);
     ensure(result_ == nullptr);
+
+    GEOSCurveToLineParams_setTolerance_r(ctxt_, curveToLineParams_, GEOS_CURVETOLINE_STEP_DEGREES, 1);
+    GEOSContext_setCurveToLineParams_r(ctxt_, curveToLineParams_);
+
+    // Input converted to line, output not converted to curve
+    result_ = GEOSClipByRect_r(ctxt_, input_, 0, 0, 1, 1);
+    ensure(result_);
+    ensure_equals(GEOSGeomTypeId_r(ctxt_, result_), GEOS_LINESTRING);
+    GEOSGeom_destroy_r(ctxt_, result_);
+
+    // Input converted to line, output converted to curve
+    GEOSContext_setLineToCurveParams_r(ctxt_, lineToCurveParams_);
+    result_ = GEOSClipByRect_r(ctxt_, input_, 0, 0, 1, 1);
+    ensure_equals(GEOSGeomTypeId_r(ctxt_, result_), GEOS_CIRCULARSTRING);
 }
 
 /// Point Z inside
diff --git a/tests/unit/capi/GEOSClusterTest.cpp b/tests/unit/capi/GEOSClusterTest.cpp
index c8136720f..bb9b91449 100644
--- a/tests/unit/capi/GEOSClusterTest.cpp
+++ b/tests/unit/capi/GEOSClusterTest.cpp
@@ -4,6 +4,7 @@
 #include <tut/tut.hpp>
 // geos
 #include <geos_c.h>
+#include <iostream>
 
 #include "capi_test_utils.h"
 
@@ -163,6 +164,61 @@ void object::test<2>()
     }
 }
 
+template<>
+template<>
+void object::test<3>()
+{
+    set_test_name("curved inputs");
+    useContext();
+
+    input_ = fromWKT(
+                 "GEOMETRYCOLLECTION ("
+                 "POINT (-2 0),"
+                 "POINT (2 0),"
+                 "CIRCULARSTRING (-1 0, 0 1, 1 0),"
+                 "LINESTRING (0 0, 1 0)"
+                 ")");
+
+    // EnvelopeDistance supports curves
+    {
+        GEOSClusterInfo* clusters = GEOSClusterEnvelopeDistance_r(ctxt_, input_, 1);
+        ensure_equals("EnvelopeDistance=1", GEOSClusterInfo_getNumClusters_r(ctxt_, clusters), 1u);
+        GEOSClusterInfo_destroy_r(ctxt_, clusters);
+    }
+
+    // EnvelopeIntersects supports curves
+    {
+        GEOSClusterInfo* clusters = GEOSClusterEnvelopeIntersects_r(ctxt_, input_);
+        ensure_equals("EnvelopeIntersects", GEOSClusterInfo_getNumClusters_r(ctxt_, clusters), 3u);
+        GEOSClusterInfo_destroy_r(ctxt_, clusters);
+    }
+
+    // These cluster methods do not support curves
+    ensure(GEOSClusterDBSCAN_r(ctxt_, input_, 0.9, 0) == nullptr);
+    ensure(GEOSClusterGeometryDistance_r(ctxt_, input_, 0.9) == nullptr);
+    ensure(GEOSClusterGeometryIntersects_r(ctxt_, input_) == nullptr);
+
+    useCurveConversion();
+
+    {
+        GEOSClusterInfo* clusters = GEOSClusterDBSCAN_r(ctxt_, input_, 0.9, 0);
+        ensure_equals("DBSCAN", GEOSClusterInfo_getNumClusters_r(ctxt_, clusters), 3u);
+        GEOSClusterInfo_destroy_r(ctxt_, clusters);
+    }
+
+    {
+        GEOSClusterInfo* clusters = GEOSClusterGeometryDistance_r(ctxt_, input_, 0.9);
+        ensure_equals("GeometryDistance", GEOSClusterInfo_getNumClusters_r(ctxt_, clusters), 3u);
+        GEOSClusterInfo_destroy_r(ctxt_, clusters);
+    }
+
+    {
+        GEOSClusterInfo* clusters = GEOSClusterGeometryIntersects_r(ctxt_, input_);
+        ensure_equals("GeometryIntersects", GEOSClusterInfo_getNumClusters_r(ctxt_, clusters), 3u);
+        GEOSClusterInfo_destroy_r(ctxt_, clusters);
+    }
+}
+
 
 } // namespace tut
 
diff --git a/tests/unit/capi/GEOSConcaveHullOfPolygonsTest.cpp b/tests/unit/capi/GEOSConcaveHullOfPolygonsTest.cpp
index 751e4cdc4..fe12e4f2d 100644
--- a/tests/unit/capi/GEOSConcaveHullOfPolygonsTest.cpp
+++ b/tests/unit/capi/GEOSConcaveHullOfPolygonsTest.cpp
@@ -1,5 +1,3 @@
-//
-// Test Suite for C-API GEOSConcaveHull
 
 #include <tut/tut.hpp>
 // geos
@@ -53,11 +51,27 @@ template<>
 template<>
 void object::test<3>()
 {
-    input_ = fromWKT("CURVEPOLYGON (COMPOUNDCURVE( CIRCULARSTRING (0 0, 1 1, 2 0), (2 0, 0 0) ))");
+    set_test_name("curved inputs");
+    useContext();
+
+    input_ = fromWKT("MULTISURFACE ("
+        "CURVEPOLYGON (CIRCULARSTRING (0 0, 1 1, 2 0, 1 -1, 0 0)),"
+        "CURVEPOLYGON (CIRCULARSTRING (4 0, 5 1, 6 0, 5 -1, 4 0)),"
+        "CURVEPOLYGON (CIRCULARSTRING (2 2, 3 3, 4 2, 3 1, 2 2))"
+    ")");
     ensure(input_ != nullptr);
 
-    result_ = GEOSConcaveHullOfPolygons(input_, 0.7, false, false);
+    printf("%s", toWKT(input_).c_str());
+
+    result_ = GEOSConcaveHullOfPolygons_r(ctxt_, input_, 0.7, false, false);
     ensure("curved geometry not supported", result_ == nullptr);
+
+    useCurveConversion();
+
+    result_ = GEOSConcaveHullOfPolygons_r(ctxt_, input_, 0.3, false, false);
+    ensure(result_);
+
+    ensure_equals(GEOSGeomTypeId_r(ctxt_, result_), GEOS_POLYGON);
 }
   
 template<>
diff --git a/tests/unit/capi/GEOSConcaveHullTest.cpp b/tests/unit/capi/GEOSConcaveHullTest.cpp
index 2984418ed..a995db9e0 100644
--- a/tests/unit/capi/GEOSConcaveHullTest.cpp
+++ b/tests/unit/capi/GEOSConcaveHullTest.cpp
@@ -54,21 +54,42 @@ template<>
 template<>
 void object::test<3>()
 {
-    input_ = fromWKT("CIRCULARSTRING (0 0, 1 1, 2 0)");
+    set_test_name("curved inputs");
+    useContext();
+
+    input_ = fromWKT("CURVEPOLYGON (CIRCULARSTRING (0 0, 1 1, 2 0, 1 0.8, 0 0))");
     ensure(input_ != nullptr);
 
-    result_ = GEOSConcaveHull(input_, 0, 0);
+    result_ = GEOSConcaveHull_r(ctxt_, input_, 0.5, 0);
     ensure(result_ == nullptr);
+
+    result_ = GEOSConcaveHullByLength_r(ctxt_, input_, 10, 0);
+    ensure(result_ == nullptr);
+
+    useCurveConversion();
+
+    result_ = GEOSConcaveHull_r(ctxt_, input_, 0.5, 0);
+    ensure(result_);
+    ensure_equals(GEOSGeomTypeId_r(ctxt_, result_), GEOS_POLYGON);
+    GEOSGeom_destroy_r(ctxt_, result_);
+
+    result_ = GEOSConcaveHullByLength_r(ctxt_, input_, 10, 0);
+    ensure(result_);
+    ensure_equals(GEOSGeomTypeId_r(ctxt_, result_), GEOS_POLYGON);
 }
 
 template<>
 template<>
 void object::test<4>()
 {
+    set_test_name("MULTIPOINT ZM");
+
     input_ = fromWKT("MULTIPOINT ZM (0 0 1 2, 1 0 3 4, 1 1 5 6, 1 8 11 4, 0.5 0.5 -4 -7)");
     result_ = GEOSConcaveHull(input_, 0, 0);
 
-    printf("%s", toWKT(result_).c_str());
+    ensure(result_);
+    ensure(GEOSHasZ(result_));
+    ensure(!GEOSHasM(result_));
 }
 
 
diff --git a/tests/unit/capi/GEOSConstrainedDelaunayTriangulationTest.cpp b/tests/unit/capi/GEOSConstrainedDelaunayTriangulationTest.cpp
index 51362108f..649a442d8 100644
--- a/tests/unit/capi/GEOSConstrainedDelaunayTriangulationTest.cpp
+++ b/tests/unit/capi/GEOSConstrainedDelaunayTriangulationTest.cpp
@@ -72,11 +72,21 @@ template<>
 template<>
 void object::test<4>()
 {
+   set_test_name("curved inputs");
+    useContext();
+
     input_ = fromWKT("CURVEPOLYGON (COMPOUNDCURVE (CIRCULARSTRING (0 0, 1 1, 2 0), (2 0, 0 0)))");
     ensure(input_ != nullptr);
 
-    result_ = GEOSConstrainedDelaunayTriangulation(input_);
+    result_ = GEOSConstrainedDelaunayTriangulation_r(ctxt_, input_);
     ensure("curved geometry not supported", result_ == nullptr);
+
+    useCurveConversion();
+
+    result_ = GEOSConstrainedDelaunayTriangulation_r(ctxt_, input_);
+    ensure(result_);
+
+    ensure_equals(GEOSGeomTypeId_r(ctxt_, result_), GEOS_GEOMETRYCOLLECTION);
 }
 
 template<>
diff --git a/tests/unit/capi/GEOSContainsTest.cpp b/tests/unit/capi/GEOSContainsTest.cpp
index 3d377e829..e7132183b 100644
--- a/tests/unit/capi/GEOSContainsTest.cpp
+++ b/tests/unit/capi/GEOSContainsTest.cpp
@@ -192,15 +192,20 @@ template<>
 template<>
 void object::test<6>()
 {
-    geom1_ = fromWKT("CIRCULARSTRING (0 0, 1 1, 2 0)");
-    geom2_ = fromWKT("LINESTRING (1 0, 2 0)");
+    set_test_name("GEOSContains with automatic linearization");
+    useContext();
+
+    geom1_ = fromWKT("CURVEPOLYGON (COMPOUNDCURVE (CIRCULARSTRING (0 0, 1 1, 2 0), (2 0, 0 0)))");
+    geom2_ = fromWKT("LINESTRING (1 0.5, 1 0.6)");
 
     ensure(geom1_);
     ensure(geom2_);
 
-    ensure_equals("curved geometry not supported", GEOSContains(geom1_, geom2_), 2);
+    ensure_equals(GEOSContains_r(ctxt_, geom1_, geom2_), 2);
+    useCurveConversion();
+    ensure_equals(GEOSContains_r(ctxt_, geom1_, geom2_), 1);
+    ensure_equals(GEOSContains_r(ctxt_, geom2_, geom1_), 0);
 }
 
-
 } // namespace tut
 
diff --git a/tests/unit/capi/GEOSConvexHullTest.cpp b/tests/unit/capi/GEOSConvexHullTest.cpp
index adb679cca..020664b70 100644
--- a/tests/unit/capi/GEOSConvexHullTest.cpp
+++ b/tests/unit/capi/GEOSConvexHullTest.cpp
@@ -43,11 +43,20 @@ template<>
 template<>
 void object::test<2>()
 {
+    set_test_name("curved inputs");
+    useContext();
+
     input_ = fromWKT("CIRCULARSTRING (0 0, 1 1, 2 0)");
     ensure(input_ != nullptr);
 
-    result_ = GEOSConvexHull(input_);
+    result_ = GEOSConvexHull_r(ctxt_, input_);
     ensure(result_ == nullptr);
+
+    useCurveConversion();
+    result_ = GEOSConvexHull_r(ctxt_, input_);
+    ensure(result_ != nullptr);
+
+    ensure_equals(GEOSGeomTypeId_r(ctxt_, result_), GEOS_POLYGON);
 }
 
 } // namespace tut
diff --git a/tests/unit/capi/GEOSCoverageCleanTest.cpp b/tests/unit/capi/GEOSCoverageCleanTest.cpp
new file mode 100644
index 000000000..1e005b28f
--- /dev/null
+++ b/tests/unit/capi/GEOSCoverageCleanTest.cpp
@@ -0,0 +1,60 @@
+#include <tut/tut.hpp>
+// geos
+#include <geos_c.h>
+
+#include "capi_test_utils.h"
+
+namespace tut {
+//
+// Test Group
+//
+
+struct test_geoscoverageclean_data : public capitest::utility
+{
+    GEOSCoverageCleanParams* ccp_ = nullptr;
+
+    ~test_geoscoverageclean_data() override {
+        if (ctxt_) {
+            GEOSCoverageCleanParams_destroy_r(ctxt_, ccp_);
+        } else {
+            GEOSCoverageCleanParams_destroy(ccp_);
+        }
+    }
+};
+
+typedef test_group<test_geoscoverageclean_data> group;
+typedef group::object object;
+
+group test_geoscoverageclean("capi::GEOSCoverageClean");
+
+template<>
+template<>
+void object::test<1>()
+{
+    set_test_name("curved inputs");
+    useContext();
+
+    input_ = fromWKT("GEOMETRYCOLLECTION ("
+                     "POLYGON ((0 0, 10 0, 10.0001 5, 10 10, 0 10, 0 0)),"
+                     "CURVEPOLYGON (COMPOUNDCURVE ((20 0, 10 0, 10 10, 20 10), CIRCULARSTRING (20 10, 25 5, 20 0))))");
+
+    result_ = GEOSCoverageClean_r(ctxt_, input_);
+    ensure(!result_);
+
+    ccp_ = GEOSCoverageCleanParams_create_r(ctxt_);
+    result_ = GEOSCoverageCleanWithParams_r(ctxt_, input_, ccp_);
+    ensure(!result_);
+
+    useCurveConversion();
+
+    result_ = GEOSCoverageClean_r(ctxt_, input_);
+    ensure(result_);
+
+    expected_ = fromWKT("GEOMETRYCOLLECTION ("
+                     "POLYGON ((0 0, 10 0, 10 10, 0 10, 0 0)),"
+                     "CURVEPOLYGON (COMPOUNDCURVE ((10 10, 20 10), CIRCULARSTRING (20 10, 25 5, 20 0), (20 0, 10 0, 10 10))))");
+
+    ensure_geometry_equals(result_, expected_);
+}
+} // namespace tut
+
diff --git a/tests/unit/capi/GEOSCoverageIsValidTest.cpp b/tests/unit/capi/GEOSCoverageIsValidTest.cpp
index 8f1e02889..2cf088868 100644
--- a/tests/unit/capi/GEOSCoverageIsValidTest.cpp
+++ b/tests/unit/capi/GEOSCoverageIsValidTest.cpp
@@ -77,22 +77,25 @@ template<> void object::test<2>
 
     expected_ = fromWKT(expectedWKT);
 
-    // std::cout << toWKT(result_) << std::endl;
-    // std::cout << toWKT(expected_) << std::endl;
-
     ensure_geometry_equals(result_, expected_, 0.01);
 }
 
 template<>
-template<> void object::test<3>
+template<>
+void object::test<3>
 ()
 {
-    input_ = fromWKT("GEOMETRYCOLLECTION ( "
-        "CURVEPOLYGON (COMPOUNDCURVE ( CIRCULARSTRING (2 0, 1 1, 2 2), (2 2, 0 2, 0 0, 2 0))), "
-        "CURVEPOLYGON (COMPOUNDCURVE ( CIRCULARSTRING (2 2, 1 1, 2 0), (2 0, 4 0, 4 2, 2 2))))");
-    ensure(input_);
+    set_test_name("curved inputs");
+    useContext();
 
-    ensure_equals("curved geometry not supported", GEOSCoverageIsValid(input_, 0, nullptr), 2);
+    input_ = fromWKT("GEOMETRYCOLLECTION ( "
+        "CURVEPOLYGON (COMPOUNDCURVE ( (4 0, 0 0, 0 4, 4 4), CIRCULARSTRING (4 4, 2 2, 4 0))), "
+        "CURVEPOLYGON (CIRCULARSTRING (4 4, 6 2, 4 0, 2 2, 4 4)))");
+
+    ensure_equals("curved geometry not supported", GEOSCoverageIsValid_r(ctxt_, input_, 0, nullptr), 2);
+
+    useCurveConversion();
+    ensure_equals(GEOSCoverageIsValid_r(ctxt_, input_, 0, nullptr), 1);
 }
 
 } // namespace tut
diff --git a/tests/unit/capi/GEOSCoverageSimplifyTest.cpp b/tests/unit/capi/GEOSCoverageSimplifyTest.cpp
index 4ca7e9c39..d22ae9b79 100644
--- a/tests/unit/capi/GEOSCoverageSimplifyTest.cpp
+++ b/tests/unit/capi/GEOSCoverageSimplifyTest.cpp
@@ -96,16 +96,28 @@ template<> void object::test<3>
 }
 
 template<>
-template<> void object::test<4>
+template<>
+void object::test<4>
 ()
 {
-    input_ = fromWKT("GEOMETRYCOLLECTION ( "
-        "CURVEPOLYGON (COMPOUNDCURVE ( CIRCULARSTRING (2 0, 1 1, 2 2), (2 2, 0 2, 0 0, 2 0))), "
-        "CURVEPOLYGON (COMPOUNDCURVE ( CIRCULARSTRING (2 2, 1 1, 2 0), (2 0, 4 0, 4 2, 2 2))))");
-    ensure(input_);
+    set_test_name("curved inputs");
+    useContext();
 
-    result_ = GEOSCoverageSimplifyVW(input_, 0.1, false);
+    input_ = fromWKT("GEOMETRYCOLLECTION ( "
+        "CURVEPOLYGON (COMPOUNDCURVE ( (4 0, 0 0, 0 4, 4 4), CIRCULARSTRING (4 4, 3.95 2, 4 0))), "
+        "CURVEPOLYGON (CIRCULARSTRING (4 4, 6 2, 4 0, 3.95 2, 4 4)))");
+
+    result_ = GEOSCoverageSimplifyVW_r(ctxt_, input_, 1, false);
     ensure("curved geometry not supported", result_ == nullptr);
+
+    useCurveConversion();
+
+    result_ = GEOSCoverageSimplifyVW_r(ctxt_, input_, 0.2, false);
+    ensure(result_);
+
+    ensure_equals(GEOSGetNumGeometries_r(ctxt_, result_), 2);
+    ensure_equals(GEOSGeomTypeId_r(ctxt_, GEOSGetGeometryN_r(ctxt_, result_, 0)), GEOS_POLYGON);
+    ensure_equals(GEOSGeomTypeId_r(ctxt_, GEOSGetGeometryN_r(ctxt_, result_, 1)), GEOS_CURVEPOLYGON);
 }
 
 
diff --git a/tests/unit/capi/GEOSCoverageUnionTest.cpp b/tests/unit/capi/GEOSCoverageUnionTest.cpp
index 1e0d7d67d..8bc1ca186 100644
--- a/tests/unit/capi/GEOSCoverageUnionTest.cpp
+++ b/tests/unit/capi/GEOSCoverageUnionTest.cpp
@@ -78,13 +78,27 @@ template<>
 template<> void object::test<4>
 ()
 {
-    input_ = fromWKT("GEOMETRYCOLLECTION ( "
-        "CURVEPOLYGON (COMPOUNDCURVE ( CIRCULARSTRING (2 0, 1 1, 2 2), (2 2, 0 2, 0 0, 2 0))), "
-        "CURVEPOLYGON (COMPOUNDCURVE ( CIRCULARSTRING (2 2, 1 1, 2 0), (2 0, 4 0, 4 2, 2 2))))");
-    ensure(input_);
+    set_test_name("curved inputs");
+    useContext();
 
-    result_ = GEOSCoverageSimplifyVW(input_, 0.1, false);
+    input_ = fromWKT("GEOMETRYCOLLECTION ( "
+        "CURVEPOLYGON (COMPOUNDCURVE ( (4 0, 0 0, 0 4, 4 4), CIRCULARSTRING (4 4, 2 2, 4 0))), "
+        "CURVEPOLYGON (CIRCULARSTRING (4 4, 6 2, 4 0, 2 2, 4 4)))");
+
+    result_ = GEOSCoverageUnion_r(ctxt_, input_);
     ensure("curved geometry not supported", result_ == nullptr);
+
+    // Input converted to line, output not converted to curve
+    GEOSContext_setCurveToLineParams_r(ctxt_, curveToLineParams_);
+    result_ = GEOSCoverageUnion_r(ctxt_, input_);
+    ensure(result_);
+    ensure_equals(GEOSGeomTypeId_r(ctxt_, result_), GEOS_POLYGON);
+    GEOSGeom_destroy_r(ctxt_, result_);
+
+    // Input converted to line, output converted to curve
+    GEOSContext_setLineToCurveParams_r(ctxt_, lineToCurveParams_);
+    result_ = GEOSCoverageUnion_r(ctxt_, input_);
+    ensure_equals(GEOSGeomTypeId_r(ctxt_, result_), GEOS_CURVEPOLYGON);
 }
 
 } // namespace tut
diff --git a/tests/unit/capi/GEOSCoveredByTest.cpp b/tests/unit/capi/GEOSCoveredByTest.cpp
index 6982fb496..68a6bed6f 100644
--- a/tests/unit/capi/GEOSCoveredByTest.cpp
+++ b/tests/unit/capi/GEOSCoveredByTest.cpp
@@ -36,13 +36,19 @@ template<>
 template<>
 void object::test<2>()
 {
-    geom1_ = fromWKT("LINESTRING (5 3, 5 4)");
-    geom2_ = fromWKT("CURVEPOLYGON (COMPOUNDCURVE (CIRCULARSTRING (0 0, 10 10, 20 0), (20 0, 0 0)))");
+    set_test_name("GEOSCoveredBy with automatic linearization");
+    useContext();
+
+    geom1_ = fromWKT("CURVEPOLYGON (COMPOUNDCURVE (CIRCULARSTRING (0 0, 1 1, 2 0), (2 0, 0 0)))");
+    geom2_ = fromWKT("LINESTRING (1 0.5, 1 0.6)");
 
     ensure(geom1_);
     ensure(geom2_);
 
-    ensure_equals("curved geometry not supported", GEOSCoveredBy(geom1_, geom2_), 2);
+    ensure_equals(GEOSCoveredBy_r(ctxt_, geom2_, geom1_), 2);
+    useCurveConversion();
+    ensure_equals(GEOSCoveredBy_r(ctxt_, geom1_, geom2_), 0);
+    ensure_equals(GEOSCoveredBy_r(ctxt_, geom2_, geom1_), 1);
 }
 
 } // namespace tut
diff --git a/tests/unit/capi/GEOSCoversTest.cpp b/tests/unit/capi/GEOSCoversTest.cpp
index bdf5f3b08..e3c9d2794 100644
--- a/tests/unit/capi/GEOSCoversTest.cpp
+++ b/tests/unit/capi/GEOSCoversTest.cpp
@@ -36,13 +36,19 @@ template<>
 template<>
 void object::test<2>()
 {
-    geom1_ = fromWKT("CURVEPOLYGON (COMPOUNDCURVE (CIRCULARSTRING (0 0, 10 10, 20 0), (20 0, 0 0)))");
-    geom2_ = fromWKT("LINESTRING (5 3, 5 4)");
+    set_test_name("GEOSCovers with automatic linearization");
+    useContext();
+
+    geom1_ = fromWKT("CURVEPOLYGON (COMPOUNDCURVE (CIRCULARSTRING (0 0, 1 1, 2 0), (2 0, 0 0)))");
+    geom2_ = fromWKT("LINESTRING (1 0.5, 1 0.6)");
 
     ensure(geom1_);
     ensure(geom2_);
 
-    ensure_equals("curved geometry not supported", GEOSCovers(geom1_, geom2_), 2);
+    ensure_equals(GEOSCovers_r(ctxt_, geom1_, geom2_), 2);
+    useCurveConversion();
+    ensure_equals(GEOSCovers_r(ctxt_, geom1_, geom2_), 1);
+    ensure_equals(GEOSCovers_r(ctxt_, geom2_, geom1_), 0);
 }
 
 } // namespace tut
diff --git a/tests/unit/capi/GEOSCrossesTest.cpp b/tests/unit/capi/GEOSCrossesTest.cpp
index 12ba5f161..6927eb621 100644
--- a/tests/unit/capi/GEOSCrossesTest.cpp
+++ b/tests/unit/capi/GEOSCrossesTest.cpp
@@ -36,14 +36,22 @@ template<>
 template<>
 void object::test<2>()
 {
+    set_test_name("GEOSCrosses with automatic linearization");
+    useContext();
+
     geom1_ = fromWKT("CIRCULARSTRING (0 0, 1 1, 2 0)");
     geom2_ = fromWKT("LINESTRING (1 0, 2 1)");
 
     ensure(geom1_);
     ensure(geom2_);
 
-    ensure_equals("curved geometry not supported", GEOSCrosses(geom1_, geom2_), 2);
-    ensure_equals("curved geometry not supported", GEOSCrosses(geom2_, geom1_), 2);
+    ensure_equals(GEOSCrosses_r(ctxt_, geom1_, geom2_), 2);
+    ensure_equals(GEOSCrosses_r(ctxt_, geom2_, geom1_), 2);
+
+    useCurveConversion();
+
+    ensure_equals(GEOSCrosses_r(ctxt_, geom1_, geom2_), 1);
+    ensure_equals(GEOSCrosses_r(ctxt_, geom2_, geom1_), 1);
 }
 
 } // namespace tut
diff --git a/tests/unit/capi/GEOSDelaunayTriangulationTest.cpp b/tests/unit/capi/GEOSDelaunayTriangulationTest.cpp
index f0f2ab077..a55ec6a90 100644
--- a/tests/unit/capi/GEOSDelaunayTriangulationTest.cpp
+++ b/tests/unit/capi/GEOSDelaunayTriangulationTest.cpp
@@ -127,11 +127,16 @@ template<>
 template<>
 void object::test<7>()
 {
+    set_test_name("curved inputs");
+
     input_ = fromWKT("CIRCULARSTRING (0 0, 1 1, 2 0, 3 -1, 4 0)");
     ensure(input_);
 
     result_ = GEOSDelaunayTriangulation(input_, 0, 0);
-    ensure("curved geometry not supported", result_ == nullptr);
+    ensure(result_);
+
+    ensure_equals(GEOSGeomTypeId(result_), GEOS_GEOMETRYCOLLECTION);
+    ensure_equals(GEOSGetNumGeometries(result_), 4);
 }
 
 template<>
diff --git a/tests/unit/capi/GEOSDensifyTest.cpp b/tests/unit/capi/GEOSDensifyTest.cpp
index 752441ddb..43e87d569 100644
--- a/tests/unit/capi/GEOSDensifyTest.cpp
+++ b/tests/unit/capi/GEOSDensifyTest.cpp
@@ -163,11 +163,19 @@ template<>
 template<>
 void object::test<10>()
 {
+    set_test_name("curved inputs");
+    useContext();
+
     input_ = fromWKT("CIRCULARSTRING (0 0, 1 1, 2 0)");
     ensure(input_);
 
-    result_ = GEOSDensify(input_, 0.1);
+    result_ = GEOSDensify_r(ctxt_, input_, 0.1);
     ensure("curved geometries not supported", result_ == nullptr);
+
+    useCurveConversion();
+    result_ = GEOSDensify_r(ctxt_, input_, 0.1);
+    ensure(result_);
+    ensure_equals(GEOSGeomTypeId_r(ctxt_, result_), GEOS_LINESTRING);
 }
 
 // Densify a LINESTRING Z, check that Z gets interpolated
diff --git a/tests/unit/capi/GEOSDifferenceTest.cpp b/tests/unit/capi/GEOSDifferenceTest.cpp
index 841f49188..ee1e8c823 100644
--- a/tests/unit/capi/GEOSDifferenceTest.cpp
+++ b/tests/unit/capi/GEOSDifferenceTest.cpp
@@ -68,8 +68,7 @@ void object::test<3>()
     geom1_ = fromWKT("CIRCULARSTRING (0 0, 1 1, 2 0)");
     geom2_ = fromWKT("LINESTRING (1 0, 2 1)");
 
-    ensure(geom1_);
-    ensure(geom2_);
+    GEOSSetSRID_r(ctxt_, geom1_, 4326);
 
     result_ = GEOSDifference(geom1_, geom2_);
     ensure(result_);
diff --git a/tests/unit/capi/GEOSDisjointSubsetUnionTest.cpp b/tests/unit/capi/GEOSDisjointSubsetUnionTest.cpp
index 76e2a3992..54c64558a 100644
--- a/tests/unit/capi/GEOSDisjointSubsetUnionTest.cpp
+++ b/tests/unit/capi/GEOSDisjointSubsetUnionTest.cpp
@@ -48,12 +48,36 @@ void object::test<2>()
 
 template <>
 template <>
-void object::test<3>() {
-    input_ = fromWKT("MULTICURVE ((0 0, 1 1), CIRCULARSTRING (1 1, 2 0, 3 1), (5 5, 8 8))");
+void object::test<3>()
+{
+    set_test_name("curved inputs");
+    useContext();
+
+    input_ = fromWKT("MULTISURFACE ("
+                     "CURVEPOLYGON (COMPOUNDCURVE(CIRCULARSTRING (0 0, 5 5, 10 0), (10 0, 10 -10, 0 -10, 0 0))),"
+                     "((10 -10, 20 -10, 20 0, 10 0, 10 -10)),"
+                     "CURVEPOLYGON (CIRCULARSTRING (100 100, 110 110, 120 100, 110 90, 100 100)))");
     ensure(input_);
 
-    result_ = GEOSDisjointSubsetUnion(input_);
+    result_ = GEOSDisjointSubsetUnion_r(ctxt_, input_);
     ensure("curved geometry not supported", result_ == nullptr);
+
+    GEOSContext_setCurveToLineParams_r(ctxt_, curveToLineParams_);
+
+    // Input converted to line, output not converted to curve
+    result_ = GEOSDisjointSubsetUnion_r(ctxt_, input_);
+    ensure(result_);
+    ensure_equals(GEOSGeomTypeId_r(ctxt_, result_), GEOS_MULTIPOLYGON);
+    ensure_equals(GEOSGetNumGeometries_r(ctxt_, result_), 2);
+    GEOSGeom_destroy_r(ctxt_, result_);
+
+    // Input converted to line, output converted to curve
+    GEOSContext_setLineToCurveParams_r(ctxt_, lineToCurveParams_);
+    result_ = GEOSDisjointSubsetUnion_r(ctxt_, input_);
+    ensure_equals(GEOSGeomTypeId_r(ctxt_, result_), GEOS_MULTISURFACE);
+    ensure_equals(GEOSGetNumGeometries_r(ctxt_, result_), 2);
+    ensure_equals(GEOSGeomTypeId_r(ctxt_, GEOSGetGeometryN_r(ctxt_, result_, 0)), GEOS_CURVEPOLYGON);
+    ensure_equals(GEOSGeomTypeId_r(ctxt_, GEOSGetGeometryN_r(ctxt_, result_, 1)), GEOS_CURVEPOLYGON);
 }
 
 } // namespace tut
diff --git a/tests/unit/capi/GEOSDisjointTest.cpp b/tests/unit/capi/GEOSDisjointTest.cpp
index f17faca3a..19b8fe138 100644
--- a/tests/unit/capi/GEOSDisjointTest.cpp
+++ b/tests/unit/capi/GEOSDisjointTest.cpp
@@ -42,5 +42,21 @@ void object::test<2>()
     ensure_equals("curved geometry not supported", GEOSDisjoint(geom2_, geom1_), 2);
 }
 
+template<>
+template<>
+void object::test<3>()
+{
+    set_test_name("GEOSDisjoint with automatic linearization");
+
+    useContext();
+
+    geom1_ = fromWKT("CIRCULARSTRING (0 0, 1 1, 2 0)");
+    geom2_ = fromWKT("CIRCULARSTRING (2 0, 3 -1, 4 0)");
+
+    ensure_equals(2, GEOSDisjoint_r(ctxt_, geom1_, geom2_));
+    useCurveConversion();
+    ensure_equals(0, GEOSDisjoint_r(ctxt_, geom1_, geom2_));
+}
+
 } // namespace tut
 
diff --git a/tests/unit/capi/GEOSEnvelopeTest.cpp b/tests/unit/capi/GEOSEnvelopeTest.cpp
index 68f9317bf..ca80f5cc3 100644
--- a/tests/unit/capi/GEOSEnvelopeTest.cpp
+++ b/tests/unit/capi/GEOSEnvelopeTest.cpp
@@ -104,5 +104,15 @@ void object::test<7>()
     );
 }
 
+template<>
+template<>
+void object::test<9>()
+{
+    set_test_name("curved inputs");
+
+    checkEnvelope("CIRCULARSTRING (0 0, 1 1, 2 0)",
+        "POLYGON ((0 0, 2 0, 2 1, 0 1, 0 0))");
+}
+
 } // namespace tut
 
diff --git a/tests/unit/capi/GEOSEqualsExactTest.cpp b/tests/unit/capi/GEOSEqualsExactTest.cpp
new file mode 100644
index 000000000..9c0671dd7
--- /dev/null
+++ b/tests/unit/capi/GEOSEqualsExactTest.cpp
@@ -0,0 +1,46 @@
+//
+// Test Suite for C-API GEOSEquals
+
+#include <tut/tut.hpp>
+// geos
+#include <geos/constants.h>
+#include <geos_c.h>
+// std
+#include <cstring>
+
+#include "capi_test_utils.h"
+
+namespace tut {
+//
+// Test Group
+//
+
+// Common data used in test cases.
+struct test_capigeosequalsexact_data : public capitest::utility {};
+
+typedef test_group<test_capigeosequalsexact_data> group;
+typedef group::object object;
+
+group test_capigeosequalsexact_group("capi::GEOSEqualsExact");
+
+//
+// Test Cases
+//
+
+template<>
+template<>
+void object::test<1>
+()
+{
+    set_test_name("almost-identical CircularStrings");
+
+    geom1_ = GEOSGeomFromWKT("CIRCULARSTRING (0 0, 1 1, 2 0)");
+    geom2_ = GEOSGeomFromWKT("CIRCULARSTRING (0 0, 1 1.001, 2 0)");
+
+    ensure_equals(GEOSEqualsExact(geom1_, geom2_, 0), 0);
+    ensure_equals(GEOSEqualsExact(geom1_, geom2_, 1e-6), 0);
+    ensure_equals(GEOSEqualsExact(geom1_, geom2_, 1e-3), 1);
+}
+
+} // namespace tut
+
diff --git a/tests/unit/capi/GEOSEqualsTest.cpp b/tests/unit/capi/GEOSEqualsTest.cpp
index 392e2cd32..547c5a311 100644
--- a/tests/unit/capi/GEOSEqualsTest.cpp
+++ b/tests/unit/capi/GEOSEqualsTest.cpp
@@ -135,14 +135,23 @@ template<>
 template<>
 void object::test<7>()
 {
+    set_test_name("GEOSEquals with automatic linearization");
+
+    useContext();
+
     geom1_ = fromWKT("CIRCULARSTRING (0 0, 1 1, 2 0)");
     geom2_ = fromWKT("CIRCULARSTRING (0 0, 1 1, 2 0)");
 
     ensure(geom1_);
     ensure(geom2_);
 
-    ensure_equals("curved geometry not supported", GEOSEquals(geom1_, geom2_), 2);
-    ensure_equals("curved geometry not supported", GEOSEquals(geom2_, geom1_), 2);
+    ensure_equals(GEOSEquals_r(ctxt_, geom1_, geom2_), 2);
+    ensure_equals(GEOSEquals_r(ctxt_, geom2_, geom1_), 2);
+
+    useCurveConversion();
+
+    ensure_equals(GEOSEquals_r(ctxt_, geom1_, geom2_), 1);
+    ensure_equals(GEOSEquals_r(ctxt_, geom2_, geom1_), 1);
 }
 
 } // namespace tut
diff --git a/tests/unit/capi/GEOSFrechetDistanceTest.cpp b/tests/unit/capi/GEOSFrechetDistanceTest.cpp
index 68d8dddd5..63767bb41 100644
--- a/tests/unit/capi/GEOSFrechetDistanceTest.cpp
+++ b/tests/unit/capi/GEOSFrechetDistanceTest.cpp
@@ -70,6 +70,10 @@ template<>
 template<>
 void object::test<5>()
 {
+    set_test_name("curved inputs");
+
+    useContext();
+
     geom1_ = fromWKT("CIRCULARSTRING (0 0, 1 1, 2 0)");
     geom2_ = fromWKT("LINESTRING (1 2, 2 2)");
 
@@ -77,8 +81,16 @@ void object::test<5>()
     ensure(geom2_);
 
     double dist;
-    ensure_equals("curved geometry not supported", GEOSFrechetDistance(geom1_, geom2_, &dist), 0);
-    ensure_equals("curved geometry not supported", GEOSFrechetDistance(geom2_, geom1_, &dist), 0);
+    ensure_equals(GEOSFrechetDistance_r(ctxt_, geom1_, geom2_, &dist), 0);
+    ensure_equals(GEOSFrechetDistance_r(ctxt_, geom2_, geom1_, &dist), 0);
+    ensure_equals(GEOSFrechetDistanceDensify_r(ctxt_, geom1_, geom2_, 0.5, &dist), 0);
+    ensure_equals(GEOSFrechetDistanceDensify_r(ctxt_, geom2_, geom1_, 0.5, &dist), 0);
+
+    useCurveConversion();
+    ensure_equals(GEOSFrechetDistance_r(ctxt_, geom1_, geom2_, &dist), 1);
+    ensure_equals(GEOSFrechetDistance_r(ctxt_, geom2_, geom1_, &dist), 1);
+    ensure_equals(GEOSFrechetDistanceDensify_r(ctxt_, geom1_, geom2_, 0.5, &dist), 1);
+    ensure_equals(GEOSFrechetDistanceDensify_r(ctxt_, geom2_, geom1_, 0.5, &dist), 1);
 }
 
 } // namespace tut
diff --git a/tests/unit/capi/GEOSGetCentroidTest.cpp b/tests/unit/capi/GEOSGetCentroidTest.cpp
index 2e9b17b5c..7d6442734 100644
--- a/tests/unit/capi/GEOSGetCentroidTest.cpp
+++ b/tests/unit/capi/GEOSGetCentroidTest.cpp
@@ -138,11 +138,19 @@ template<>
 template<>
 void object::test<6>()
 {
+    set_test_name("curved inputs");
+    useContext();
+
     input_ = fromWKT("CURVEPOLYGON (COMPOUNDCURVE (CIRCULARSTRING (0 0, 1 1, 2 0), (2 0, 0 0)))");
     ensure(input_ != nullptr);
 
-    result_ = GEOSGetCentroid(input_);
+    result_ = GEOSGetCentroid_r(ctxt_, input_);
     ensure("curved geometry not supported", result_ == nullptr);
+
+    useCurveConversion();
+    result_ = GEOSGetCentroid_r(ctxt_, input_);
+    ensure(result_);
+    ensure_equals(GEOSGeomTypeId_r(ctxt_, result_), GEOS_POINT);
 }
 
 template<>
diff --git a/tests/unit/capi/GEOSGridIntersectionFractionsTest.cpp b/tests/unit/capi/GEOSGridIntersectionFractionsTest.cpp
index 412f96377..5dacbf175 100644
--- a/tests/unit/capi/GEOSGridIntersectionFractionsTest.cpp
+++ b/tests/unit/capi/GEOSGridIntersectionFractionsTest.cpp
@@ -38,12 +38,17 @@ template<>
 void object::test<2>()
 {
     set_test_name("curved input");
+    useContext();
 
     input_ = fromWKT("CURVEPOLYGON ((0.5 0.5, 2.5 0.5, 2.5 2.5, 0.5 2.5, 0.5 0.5))");
 
-    std::vector<float> result_vec;
-    int result = GEOSGridIntersectionFractions(input_, 1, 0, 5, 3, 4, 3, result_vec.data());
+    std::vector<float> result_vec(12);
+    int result = GEOSGridIntersectionFractions_r(ctxt_, input_, 1, 0, 5, 3, 4, 3, result_vec.data());
     ensure_equals(result, 0);
+
+    useCurveConversion();
+    result = GEOSGridIntersectionFractions_r(ctxt_, input_, 1, 0, 5, 3, 4, 3, result_vec.data());
+    ensure_equals(result, 1);
 }
 
 template<>
diff --git a/tests/unit/capi/GEOSHausdorffDistanceTest.cpp b/tests/unit/capi/GEOSHausdorffDistanceTest.cpp
index 747d2a9df..db298ccb1 100644
--- a/tests/unit/capi/GEOSHausdorffDistanceTest.cpp
+++ b/tests/unit/capi/GEOSHausdorffDistanceTest.cpp
@@ -61,6 +61,10 @@ template<>
 template<>
 void object::test<3>()
 {
+    set_test_name("curved inputs");
+
+    useContext();
+
     geom1_ = fromWKT("CIRCULARSTRING (0 0, 1 1, 2 0)");
     geom2_ = fromWKT("LINESTRING (1 2, 2 2)");
 
@@ -68,8 +72,29 @@ void object::test<3>()
     ensure(geom2_);
 
     double dist;
-    ensure_equals("curved geometry not supported", GEOSHausdorffDistance(geom1_, geom2_, &dist), 0);
-    ensure_equals("curved geometry not supported", GEOSHausdorffDistance(geom2_, geom1_, &dist), 0);
+    double p1x, p1y, p2x, p2y;
+
+    ensure_equals(GEOSHausdorffDistance_r(ctxt_, geom1_, geom2_, &dist), 0);
+    ensure_equals(GEOSHausdorffDistance_r(ctxt_, geom2_, geom1_, &dist), 0);
+    ensure_equals(GEOSHausdorffDistanceDensify_r(ctxt_, geom1_, geom2_, 0.5, &dist), 0);
+    ensure_equals(GEOSHausdorffDistanceDensify_r(ctxt_, geom2_, geom1_, 0.5, &dist), 0);
+
+    ensure_equals(GEOSHausdorffDistanceWithPoints_r(ctxt_, geom1_, geom2_, &dist, &p1x, &p1y, &p2x, &p2y), 0);
+    ensure_equals(GEOSHausdorffDistanceWithPoints_r(ctxt_, geom2_, geom1_, &dist, &p1x, &p1y, &p2x, &p2y), 0);
+    ensure_equals(GEOSHausdorffDistanceDensifyWithPoints_r(ctxt_, geom1_, geom2_, 0.5, &dist, &p1x, &p1y, &p2x, &p2y), 0);
+    ensure_equals(GEOSHausdorffDistanceDensifyWithPoints_r(ctxt_, geom2_, geom1_, 0.5, &dist, &p1x, &p1y, &p2x, &p2y), 0);
+
+    useCurveConversion();
+    ensure_equals(GEOSHausdorffDistance_r(ctxt_, geom1_, geom2_, &dist), 1);
+    ensure_equals(GEOSHausdorffDistance_r(ctxt_, geom2_, geom1_, &dist), 1);
+    ensure_equals(GEOSHausdorffDistanceDensify_r(ctxt_, geom1_, geom2_, 0.5, &dist), 1);
+    ensure_equals(GEOSHausdorffDistanceDensify_r(ctxt_, geom2_, geom1_, 0.5, &dist), 1);
+
+    ensure_equals(GEOSHausdorffDistanceWithPoints_r(ctxt_, geom1_, geom2_, &dist, &p1x, &p1y, &p2x, &p2y), 1);
+    ensure_equals(GEOSHausdorffDistanceWithPoints_r(ctxt_, geom2_, geom1_, &dist, &p1x, &p1y, &p2x, &p2y), 1);
+    ensure_equals(GEOSHausdorffDistanceDensifyWithPoints_r(ctxt_, geom1_, geom2_, 0.5, &dist, &p1x, &p1y, &p2x, &p2y), 1);
+    ensure_equals(GEOSHausdorffDistanceDensifyWithPoints_r(ctxt_, geom2_, geom1_, 0.5, &dist, &p1x, &p1y, &p2x, &p2y), 1);
+
 }
 
 template<>
diff --git a/tests/unit/capi/GEOSInterpolateTest.cpp b/tests/unit/capi/GEOSInterpolateTest.cpp
index 5cfeec787..4096cd690 100644
--- a/tests/unit/capi/GEOSInterpolateTest.cpp
+++ b/tests/unit/capi/GEOSInterpolateTest.cpp
@@ -3,6 +3,7 @@
 #include <tut/tut.hpp>
 // geos
 #include <geos_c.h>
+#include <geos/constants.h>
 
 #include "capi_test_utils.h"
 
@@ -148,4 +149,35 @@ void object::test<10>
     ensure(result_ == nullptr);
 }
 
+template<>
+template<>
+void object::test<11>()
+{
+    set_test_name("CircularString input");
+    useContext();
+
+    input_ = fromWKT("CIRCULARSTRING (0 0, 2 2, 4 0)");
+    ensure(input_);
+
+    result_ = GEOSInterpolate_r(ctxt_, input_, geos::MATH_PI);
+    ensure(!result_);
+
+    result_ = GEOSInterpolateNormalized_r(ctxt_, input_, 0.5);
+    ensure(!result_);
+
+    useCurveConversion();
+    result_ = GEOSInterpolate_r(ctxt_, input_, geos::MATH_PI);
+    ensure(result_);
+
+    expected_ = fromWKT("POINT (2 2)");
+    ensure(expected_);
+
+    ensure_geometry_equals_exact(result_, expected_, 0.1);
+
+    geom3_ = GEOSInterpolateNormalized_r(ctxt_, input_, 0.5);
+    ensure(geom3_);
+
+    ensure_geometry_equals_exact(geom3_, expected_, 0.1);
+}
+
 } // namespace tut
diff --git a/tests/unit/capi/GEOSIntersectsTest.cpp b/tests/unit/capi/GEOSIntersectsTest.cpp
index 1beea562a..f11ac8615 100644
--- a/tests/unit/capi/GEOSIntersectsTest.cpp
+++ b/tests/unit/capi/GEOSIntersectsTest.cpp
@@ -260,13 +260,16 @@ template<>
 template<>
 void object::test<14>()
 {
-    set_test_name("curved geometries not supported by RectangleIntersects");
+    set_test_name("GEOSIntersects with automatic linearization");
 
-    geom1_ = fromWKT("POLYGON ((0 0, 10 0, 10 10, 0 10, 0 0))");
-    geom2_ = fromWKT("CIRCULARSTRING (5 0, 10 5, 15 0)");
+    useContext();
 
-    ensure_equals(GEOSIntersects(geom1_, geom2_), 2);
-    ensure_equals(GEOSIntersects(geom2_, geom1_), 2);
+    geom1_ = fromWKT("CIRCULARSTRING (0 0, 1 1, 2 0)");
+    geom2_ = fromWKT("CIRCULARSTRING (2 0, 3 -1, 4 0)");
+
+    ensure_equals(2, GEOSIntersects_r(ctxt_, geom1_, geom2_));
+    useCurveConversion();
+    ensure_equals(1, GEOSIntersects_r(ctxt_, geom1_, geom2_));
 }
 
 } // namespace tut
diff --git a/tests/unit/capi/GEOSLargestEmptyCircleTest.cpp b/tests/unit/capi/GEOSLargestEmptyCircleTest.cpp
index c47dbbb7d..5d5531b90 100644
--- a/tests/unit/capi/GEOSLargestEmptyCircleTest.cpp
+++ b/tests/unit/capi/GEOSLargestEmptyCircleTest.cpp
@@ -64,26 +64,22 @@ template<>
 template<>
 void object::test<3>()
 {
-    input_ = fromWKT("MULTICURVE (CIRCULARSTRING (0 0, 1 1, 2 0), (0 3, 2 3))");
-    ensure(input_);
+    set_test_name("curved inputs");
+    useContext();
 
-    result_ = GEOSLargestEmptyCircle(input_, nullptr, 0.001);
-
-    ensure("curved geometries not supported", result_ == nullptr);
-}
-
-template<>
-template<>
-void object::test<4>()
-{
-    input_ = GEOSGeomFromWKT("MULTILINESTRING ((40 90, 90 60), (90 40, 40 10))");
-    geom2_ = GEOSGeomFromWKT("CURVEPOLYGON (COMPOUNDCURVE (CIRCULARSTRING(0 100, 50 150, 100 100), (100 100, 100 0, 0 0, 0 100)))");
+    input_ = fromWKT("MULTILINESTRING ((40 90, 90 60), (90 40, 40 10))");
+    geom2_ = fromWKT("CURVEPOLYGON (COMPOUNDCURVE (CIRCULARSTRING(0 100, 50 150, 100 100), (100 100, 100 0, 0 0, 0 100)))");
     ensure(input_);
     ensure(geom2_);
 
-    result_ = GEOSLargestEmptyCircle(input_, geom2_, 0.001);
-
+    result_ = GEOSLargestEmptyCircle_r(ctxt_, input_, geom2_, 0.001);
     ensure("curved geometries not supported", result_ == nullptr);
+
+    useCurveConversion();
+    result_ = GEOSLargestEmptyCircle_r(ctxt_, input_, geom2_, 0.001);
+    ensure(result_);
+
+    ensure_equals(GEOSGeomTypeId_r(ctxt_, result_), GEOS_LINESTRING);
 }
 
 
diff --git a/tests/unit/capi/GEOSMakeValidTest.cpp b/tests/unit/capi/GEOSMakeValidTest.cpp
index 44ec52c84..ff7b98dfc 100644
--- a/tests/unit/capi/GEOSMakeValidTest.cpp
+++ b/tests/unit/capi/GEOSMakeValidTest.cpp
@@ -3,6 +3,7 @@
 #include <tut/tut.hpp>
 // geos
 #include <geos_c.h>
+#include <geos/constants.h>
 // std
 #include <cstring>
 #include <cstdint>
@@ -94,12 +95,39 @@ template<>
 template<>
 void object::test<5>()
 {
+    set_test_name("curved inputs, curved output");
+    useContext();
+
     // ring outside shell
     input_ = fromWKT("CURVEPOLYGON (COMPOUNDCURVE (CIRCULARSTRING (0 0, 10 10, 20 0), (20 0, 0 0)), (10 10, 30 10, 30 30, 10 10))");
     ensure(input_);
+    GEOSSetSRID_r(ctxt_, input_, 4326);
 
-    result_ = GEOSMakeValid(input_);
+
+    result_ = GEOSMakeValid_r(ctxt_, input_);
     ensure("curved geometry not supported", result_ == nullptr);
+
+    // Input converted to line, output not converted to curve
+    GEOSContext_setCurveToLineParams_r(ctxt_, curveToLineParams_);
+    result_ = GEOSMakeValid_r(ctxt_, input_);
+    ensure(result_);
+    ensure_equals(GEOSGeomTypeId_r(ctxt_, result_), GEOS_MULTIPOLYGON);
+    GEOSGeom_destroy_r(ctxt_, result_);
+
+    // Input converted to line, output converted to curve
+    GEOSContext_setLineToCurveParams_r(ctxt_, lineToCurveParams_);
+    result_ = GEOSMakeValid_r(ctxt_, input_);
+
+    ensure_equals(GEOSGeomTypeId_r(ctxt_, result_), GEOS_MULTISURFACE);
+    ensure_equals(GEOSGetNumGeometries_r(ctxt_, result_), 2);
+    ensure_equals(GEOSGeomTypeId_r(ctxt_, GEOSGetGeometryN_r(ctxt_, result_, 0u)), GEOS_CURVEPOLYGON);
+    ensure_equals(GEOSGeomTypeId_r(ctxt_, GEOSGetGeometryN_r(ctxt_, result_, 1u)), GEOS_POLYGON);
+
+    double area = -1;
+    ensure(GEOSArea_r(ctxt_, result_, &area));
+    ensure_equals("area does not match expected", area, geos::MATH_PI*10*10/2 + 20*20/2, 1e-2);
+
+    ensure_equals(GEOSGetSRID_r(ctxt_, result_), 4326);
 }
 
 template<>
diff --git a/tests/unit/capi/GEOSMaximumInscribedCircleTest.cpp b/tests/unit/capi/GEOSMaximumInscribedCircleTest.cpp
index c309d0a76..522cdc19c 100644
--- a/tests/unit/capi/GEOSMaximumInscribedCircleTest.cpp
+++ b/tests/unit/capi/GEOSMaximumInscribedCircleTest.cpp
@@ -23,9 +23,13 @@ struct test_capimaximuminscribedcircle_data : public capitest::utility {
         GEOSWKTWriter_setRoundingPrecision(wktw_, 8);
     }
 
-    ~test_capimaximuminscribedcircle_data()
+    ~test_capimaximuminscribedcircle_data() override
     {
-        GEOSFree(wkt_);
+        if (ctxt_) {
+            GEOSFree_r(ctxt_, wkt_);
+        } else {
+            GEOSFree(wkt_);
+        }
     }
 
 };
@@ -74,11 +78,20 @@ template<>
 template<>
 void object::test<3>()
 {
+    set_test_name("curved inputs");
+    useContext();
+
     input_ = fromWKT("CURVEPOLYGON (COMPOUNDCURVE (CIRCULARSTRING (0 0, 10 10, 20 0), (20 0, 0 0)))");
     ensure(input_);
 
-    result_ = GEOSMaximumInscribedCircle(input_, 1);
+    result_ = GEOSMaximumInscribedCircle_r(ctxt_, input_, 1);
     ensure("curved geometry not supported", result_ == nullptr);
+
+    useCurveConversion();
+    result_ = GEOSMaximumInscribedCircle_r(ctxt_, input_, 1);
+    ensure(result_);
+
+    ensure_equals(GEOSGeomTypeId_r(ctxt_, result_), GEOS_LINESTRING);
 }
 
 } // namespace tut
diff --git a/tests/unit/capi/GEOSMinimumBoundingCircleTest.cpp b/tests/unit/capi/GEOSMinimumBoundingCircleTest.cpp
index 3634752e9..144be2ae3 100644
--- a/tests/unit/capi/GEOSMinimumBoundingCircleTest.cpp
+++ b/tests/unit/capi/GEOSMinimumBoundingCircleTest.cpp
@@ -120,13 +120,21 @@ template<>
 void object::test<5>
 ()
 {
+    set_test_name("curved inputs");
+    useContext();
+
     input_ = fromWKT("CIRCULARSTRING (0 0, 1 1, 2 0)");
     ensure(input_);
 
-    GEOSGeometry* center;
     double radius;
-    result_ = GEOSMinimumBoundingCircle(input_, &radius, &center);
+    result_ = GEOSMinimumBoundingCircle_r(ctxt_, input_, &radius, &geom1_);
     ensure("curved geometry not supported", result_ == nullptr);
+
+    useCurveConversion();
+    result_ = GEOSMinimumBoundingCircle_r(ctxt_, input_, &radius, &geom1_);
+    ensure(result_);
+
+    ensure_equals(GEOSGeomTypeId_r(ctxt_, result_), GEOS_POLYGON);
 }
 
 template<>
diff --git a/tests/unit/capi/GEOSMinimumClearanceTest.cpp b/tests/unit/capi/GEOSMinimumClearanceTest.cpp
index 49fca2637..4c9dbe699 100644
--- a/tests/unit/capi/GEOSMinimumClearanceTest.cpp
+++ b/tests/unit/capi/GEOSMinimumClearanceTest.cpp
@@ -143,4 +143,31 @@ void object::test<7>()
     ensure_geometry_equals(result_, expected_);
 }
 
+template<>
+template<>
+void object::test<8>()
+{
+    set_test_name("curved inputs");
+    useContext();
+
+    input_ = fromWKT("CURVEPOLYGON (COMPOUNDCURVE (CIRCULARSTRING (0 0, 5 5, 10 0), (10 0, 15 0, 15 10, 10 10), CIRCULARSTRING (10 10, 5 5.1, 0 10), (0 10,  -5 10, -5 0, 0 0)))");
+    ensure(input_);
+
+    double result = -1;
+    ensure_equals(GEOSMinimumClearance_r(ctxt_, input_, &result), 2);
+    ensure(GEOSMinimumClearanceLine_r(ctxt_, input_) == nullptr);
+
+    useCurveConversion();
+    ensure_equals(GEOSMinimumClearance_r(ctxt_, input_, &result), 0);
+    ensure_equals("minimum clearance value does not match", result, 0.1, 0.01);
+
+    result_ = GEOSMinimumClearanceLine_r(ctxt_, input_);
+    ensure(result_);
+
+    expected_ = fromWKT("LINESTRING (5 5, 5 5.1)");
+    ensure(expected_);
+
+    ensure_geometry_equals(result_, expected_, 0.2);
+}
+
 } // namespace tut
diff --git a/tests/unit/capi/GEOSMinimumRotatedRectangleTest.cpp b/tests/unit/capi/GEOSMinimumRotatedRectangleTest.cpp
index b2eff83fd..55ffc166c 100644
--- a/tests/unit/capi/GEOSMinimumRotatedRectangleTest.cpp
+++ b/tests/unit/capi/GEOSMinimumRotatedRectangleTest.cpp
@@ -123,11 +123,20 @@ template<>
 void object::test<8>
 ()
 {
-    input_ = fromWKT("CIRCULARSTRING (0 0, 1 1, 2 0)");
+    set_test_name("curved inputs");
+    useContext();
+
+    input_ = fromWKT("CIRCULARSTRING (0 0, 1 1.5, 2 1)");
     ensure(input_);
 
-    result_ = GEOSMinimumRotatedRectangle(input_);
+    result_ = GEOSMinimumRotatedRectangle_r(ctxt_, input_);
     ensure("curved geometry not supported", result_ == nullptr);
+
+    useCurveConversion();
+    result_ = GEOSMinimumRotatedRectangle_r(ctxt_, input_);
+
+    ensure(result_);
+    ensure_equals(GEOSGeomTypeId_r(ctxt_, result_), GEOS_POLYGON);
 }
 
 } // namespace tut
diff --git a/tests/unit/capi/GEOSMinimumWidthTest.cpp b/tests/unit/capi/GEOSMinimumWidthTest.cpp
index 4b5658126..bb10e5ed8 100644
--- a/tests/unit/capi/GEOSMinimumWidthTest.cpp
+++ b/tests/unit/capi/GEOSMinimumWidthTest.cpp
@@ -68,11 +68,19 @@ template<>
 void object::test<3>
 ()
 {
-    input_ = fromWKT("CIRCULARSTRING (0 0, 1 1, 2 0)");
+    set_test_name("curved inputs");
+    useContext();
+
+    input_ = fromWKT("CIRCULARSTRING (0 0, 1 1.5, 2 1)");
     ensure(input_);
 
-    result_ = GEOSMinimumWidth(input_);
+    result_ = GEOSMinimumWidth_r(ctxt_, input_);
     ensure("curved geometry not supported", result_ == nullptr);
+
+    useCurveConversion();
+    result_ = GEOSMinimumWidth_r(ctxt_, input_);
+
+    ensure_equals(GEOSGeomTypeId_r(ctxt_, result_), GEOS_LINESTRING);
 }
 
 } // namespace tut
diff --git a/tests/unit/capi/GEOSNearestPointsTest.cpp b/tests/unit/capi/GEOSNearestPointsTest.cpp
index fb7581751..b3caf3001 100644
--- a/tests/unit/capi/GEOSNearestPointsTest.cpp
+++ b/tests/unit/capi/GEOSNearestPointsTest.cpp
@@ -81,6 +81,8 @@ template<>
 void object::test<2>
 ()
 {
+    set_test_name("closest points are vertices of two disjoint inputs");
+
     checkNearestPoints(
         "POLYGON((1 1,1 5,5 5,5 1,1 1))",
         "POLYGON((8 8, 9 9, 9 10, 8 8))",
@@ -94,6 +96,8 @@ template<>
 void object::test<3>
 ()
 {
+    set_test_name("point inside polygon");
+
     checkNearestPoints(
         "POLYGON((1 1,1 5,5 5,5 1,1 1))",
         "POINT(2 2)",
@@ -107,6 +111,8 @@ template<>
 void object::test<4>
 ()
 {
+    set_test_name("closest point is not a vertex of LineString");
+
     checkNearestPoints(
         "LINESTRING(1 5,5 5,5 1,1 1)",
         "POINT(2 2)",
@@ -119,6 +125,8 @@ template<>
 void object::test<5>
 ()
 {
+    set_test_name("two crossing LineStrings");
+
     checkNearestPoints(
         "LINESTRING(0 0,10 10)",
         "LINESTRING(0 10,10 0)",
@@ -131,6 +139,8 @@ template<>
 void object::test<6>
 ()
 {
+    set_test_name("LineString partially inside Polygon");
+
     checkNearestPoints(
         "POLYGON((0 0,10 0,10 10,0 10,0 0))",
         "LINESTRING(8 5,12 5)",
@@ -141,8 +151,7 @@ void object::test<6>
 
 template<>
 template<>
-void object::test<7>()
-{
+void object::test<7>() {
     set_test_name("2D points returned for 4D inputs");
 
     geom1_ = fromWKT("POINT ZM (0 0 1 2)");
@@ -159,5 +168,33 @@ void object::test<7>()
     GEOSCoordSeq_destroy(coords);
 }
 
+template<>
+template<>
+void object::test<8>
+()
+{
+    set_test_name("curved inputs");
+
+    geom1_ = fromWKT("CIRCULARSTRING (0 0, 1 1, 2 0)");
+    geom2_ = fromWKT("LINESTRING (3 0, 4 0)");
+
+    GEOSCoordSequence* coords = GEOSNearestPoints(geom1_, geom2_);
+
+    unsigned int size;
+    ensure(GEOSCoordSeq_getSize(coords, &size));
+    ensure_equals(size, 2u);
+
+    double x1, y1, x2, y2;
+    GEOSCoordSeq_getXY(coords, 0, &x1, &y1);
+    GEOSCoordSeq_getXY(coords, 1, &x2, &y2);
+
+    ensure_equals(x1, 2);
+    ensure_equals(y1, 0);
+    ensure_equals(x2, 3);
+    ensure_equals(y2, 0);
+
+    GEOSCoordSeq_destroy(coords);
+}
+
 } // namespace tut
 
diff --git a/tests/unit/capi/GEOSOffsetCurveTest.cpp b/tests/unit/capi/GEOSOffsetCurveTest.cpp
index 242d18853..799546ddc 100644
--- a/tests/unit/capi/GEOSOffsetCurveTest.cpp
+++ b/tests/unit/capi/GEOSOffsetCurveTest.cpp
@@ -262,12 +262,19 @@ template<>
 template<>
 void object::test<13>()
 {
+    set_test_name("curved inputs");
+    useContext();
+
     input_ = fromWKT("CIRCULARSTRING (0 0, 1 1, 2 0)");
     ensure(input_);
 
-    result_ = GEOSOffsetCurve(input_, 1, 8, GEOSBUF_JOIN_ROUND, 0);
-
+    result_ = GEOSOffsetCurve_r(ctxt_, input_, 1, 8, GEOSBUF_JOIN_ROUND, 0);
     ensure("curved geometries not supported", result_ == nullptr);
+
+    useCurveConversion();
+    result_ = GEOSOffsetCurve_r(ctxt_, input_, 1, 8, GEOSBUF_JOIN_ROUND, 0);
+    ensure(result_);
+    ensure_equals(GEOSGeomTypeId_r(ctxt_, result_), GEOS_LINESTRING);
 }
 
 template<>
diff --git a/tests/unit/capi/GEOSOverlapsTest.cpp b/tests/unit/capi/GEOSOverlapsTest.cpp
index 253bd762f..7298fe1dc 100644
--- a/tests/unit/capi/GEOSOverlapsTest.cpp
+++ b/tests/unit/capi/GEOSOverlapsTest.cpp
@@ -39,14 +39,20 @@ template<>
 template<>
 void object::test<2>()
 {
-    geom1_ = fromWKT("CIRCULARSTRING (0 0, 1 1, 2 0)");
-    geom2_ = fromWKT("LINESTRING (1 0, 2 1)");
+    set_test_name("GEOSOverlaps with automatic linearization");
+    useContext();
+
+    geom1_ = fromWKT("CURVEPOLYGON (COMPOUNDCURVE (CIRCULARSTRING (0 0, 1 1, 2 0), (2 0, 0 0)))");
+    geom2_ = fromWKT("POLYGON ((1 0, 2 0, 2 1, 1 0 ))");
 
     ensure(geom1_);
     ensure(geom2_);
 
-    ensure_equals("curved geometry not supported", GEOSOverlaps(geom1_, geom2_), 2);
-    ensure_equals("curved geometry not supported", GEOSOverlaps(geom2_, geom1_), 2);
+    ensure_equals(GEOSOverlaps_r(ctxt_, geom1_, geom2_), 2);
+    ensure_equals(GEOSOverlaps_r(ctxt_, geom2_, geom1_), 2);
+    useCurveConversion();
+    ensure_equals(GEOSOverlaps_r(ctxt_, geom1_, geom2_), 1);
+    ensure_equals(GEOSOverlaps_r(ctxt_, geom2_, geom1_), 1);
 }
 
 } // namespace tut
diff --git a/tests/unit/capi/GEOSPointOnSurfaceTest.cpp b/tests/unit/capi/GEOSPointOnSurfaceTest.cpp
index de1ab2d3c..8afef2a3b 100644
--- a/tests/unit/capi/GEOSPointOnSurfaceTest.cpp
+++ b/tests/unit/capi/GEOSPointOnSurfaceTest.cpp
@@ -224,11 +224,20 @@ template<>
 void object::test<10>
 ()
 {
+    set_test_name("curved inputs");
+    useContext();
+
     input_ = fromWKT("CURVEPOLYGON (COMPOUNDCURVE(CIRCULARSTRING (0 0, 1 1, 2 0), (2 0, 0 0)))");
     ensure(input_);
 
-    result_ = GEOSPointOnSurface(input_);
+    result_ = GEOSPointOnSurface_r(ctxt_, input_);
     ensure("curved geometry not supported", result_ == nullptr);
+
+    useCurveConversion();
+    result_ = GEOSPointOnSurface_r(ctxt_, input_);
+    ensure(result_);
+
+    ensure_equals(GEOSContains_r(ctxt_, input_, result_), 1);
 }
 
 template<>
diff --git a/tests/unit/capi/GEOSPolygonHullSimplifyTest.cpp b/tests/unit/capi/GEOSPolygonHullSimplifyTest.cpp
index f769b001f..d126995bc 100644
--- a/tests/unit/capi/GEOSPolygonHullSimplifyTest.cpp
+++ b/tests/unit/capi/GEOSPolygonHullSimplifyTest.cpp
@@ -93,13 +93,43 @@ template<>
 template<>
 void object::test<6>()
 {
+    set_test_name("GEOSPolygonHullSimplify curved inputs");
+    useContext();
+
     input_ = fromWKT("CURVEPOLYGON (COMPOUNDCURVE( CIRCULARSTRING (0 0, 1 1, 2 0), (2 0, 1 0.0001, 0 0)))");
     ensure(input_);
 
-    result_ = GEOSPolygonHullSimplify(input_, 1, 0.8);
+    result_ = GEOSPolygonHullSimplify_r(ctxt_, input_, 1, 0.8);
     ensure("curved geometries not supported", result_ == nullptr);
+
+    useCurveConversion();
+    result_ = GEOSPolygonHullSimplify_r(ctxt_, input_, 1, 0.8);
+    ensure(result_);
+
+    expected_ = fromWKT("CURVEPOLYGON (COMPOUNDCURVE ((0 0, 2 0), CIRCULARSTRING (2 0, 1 1, 0 0)))");
+    ensure_geometry_equals(result_, expected_);
 }
 
+template<>
+template<>
+void object::test<7>()
+{
+    set_test_name("GEOSPolygonHullSimplifyMode curved inputs");
+    useContext();
+
+    input_ = fromWKT("CURVEPOLYGON (COMPOUNDCURVE( CIRCULARSTRING (0 0, 1 1, 2 0), (2 0, 1 0.0001, 0 0)))");
+    ensure(input_);
+
+    result_ = GEOSPolygonHullSimplifyMode_r(ctxt_, input_, 1, GEOSHULL_PARAM_AREA_RATIO, 0.8);
+    ensure("curved geometries not supported", result_ == nullptr);
+
+    useCurveConversion();
+    result_ = GEOSPolygonHullSimplifyMode_r(ctxt_, input_, 1, GEOSHULL_PARAM_AREA_RATIO, 0.8);
+    ensure(result_);
+
+    expected_ = fromWKT("CURVEPOLYGON (COMPOUNDCURVE ((0 0, 2 0), CIRCULARSTRING (2 0, 1 1, 0 0)))");
+    ensure_geometry_equals(result_, expected_);
+}
 
 } // namespace tut
 
diff --git a/tests/unit/capi/GEOSPolygonizeTest.cpp b/tests/unit/capi/GEOSPolygonizeTest.cpp
index 5de4a27c7..12b433e64 100644
--- a/tests/unit/capi/GEOSPolygonizeTest.cpp
+++ b/tests/unit/capi/GEOSPolygonizeTest.cpp
@@ -244,9 +244,6 @@ void object::test<8>
     geom1_ = fromWKT("LINESTRING ZM (0 0 5 4, 2 0 6 5, 2 2 7 6)");
     geom2_ = fromWKT("LINESTRING ZM (2 2 7 6, 0 0 5 4)");
 
-    ensure(geom1_);
-    ensure(geom2_);
-
     std::array<GEOSGeometry*, 2> geoms = {geom1_, geom2_};
     result_ = GEOSPolygonize(geoms.data(), geoms.size());
     ensure(result_);
@@ -317,5 +314,4 @@ void object::test<10>()
     GEOSGeom_destroy(expected_invalidRings);
 }
 
-} // namespace tut
-
+} // namespace tut
\ No newline at end of file
diff --git a/tests/unit/capi/GEOSProjectTest.cpp b/tests/unit/capi/GEOSProjectTest.cpp
index d2556a897..a8f550809 100644
--- a/tests/unit/capi/GEOSProjectTest.cpp
+++ b/tests/unit/capi/GEOSProjectTest.cpp
@@ -3,6 +3,7 @@
 #include <tut/tut.hpp>
 // geos
 #include <geos_c.h>
+#include <geos/constants.h>
 
 #include "capi_test_utils.h"
 
@@ -119,4 +120,27 @@ void object::test<5>
     ensure_equals("GEOSProjectNormalized", dist_norm, 0.25);
 }
 
+template<>
+template<>
+void object::test<6>
+()
+{
+    set_test_name("CircularString input");
+    useContext();
+
+    geom1_ = fromWKT("CIRCULARSTRING (0 0, 1 1, 2 0)");
+    geom2_ = fromWKT("POINT (1 1.1)");
+
+    ensure_equals(GEOSProject_r(ctxt_, geom1_, geom2_), -1.0);
+    ensure_equals(GEOSProjectNormalized_r(ctxt_, geom1_, geom2_), -1.0);
+
+    useCurveConversion();
+
+    double dist = GEOSProject_r(ctxt_, geom1_, geom2_);
+    ensure_equals("GEOSProject result does not match", dist, geos::MATH_PI/2, 1e-2);
+
+    double dist_norm = GEOSProjectNormalized_r(ctxt_, geom1_, geom2_);
+    ensure_equals("GEOSProjectNormalized result does not match", dist_norm, 0.5, 1e-3);
+}
+
 } // namespace tut
diff --git a/tests/unit/capi/GEOSRelateBoundaryNodeRuleTest.cpp b/tests/unit/capi/GEOSRelateBoundaryNodeRuleTest.cpp
index d27d4d23b..4a60bb948 100644
--- a/tests/unit/capi/GEOSRelateBoundaryNodeRuleTest.cpp
+++ b/tests/unit/capi/GEOSRelateBoundaryNodeRuleTest.cpp
@@ -21,7 +21,9 @@ struct test_capigeosrelateboundarynoderule_data : public capitest::utility {
 
     ~test_capigeosrelateboundarynoderule_data()
     {
-        GEOSFree(pat_);
+        if (pat_) {
+            GEOSFree(pat_);
+        }
     }
 };
 
@@ -136,6 +138,29 @@ void object::test<8>
     ensure(nullptr == pat_);
 }
 
+template<>
+template<>
+void object::test<9>()
+{
+    set_test_name("GEOSRelateBoundaryNodeRule with automatic linearization");
+
+    useContext();
+
+    geom1_ = fromWKT("CIRCULARSTRING (0 0, 1 1, 2 0)");
+    geom2_ = fromWKT("LINESTRING (1 0, 2 1)");
+
+    ensure(geom1_);
+    ensure(geom2_);
+
+    ensure(GEOSRelateBoundaryNodeRule_r(ctxt_, geom1_, geom2_, GEOSRELATE_BNR_MOD2) == nullptr);
+    ensure(GEOSRelateBoundaryNodeRule_r(ctxt_, geom2_, geom1_, GEOSRELATE_BNR_MOD2) == nullptr);
+
+    useCurveConversion();
+
+    char* pattern = GEOSRelateBoundaryNodeRule_r(ctxt_, geom1_, geom2_, GEOSRELATE_BNR_MOD2);
+    ensure(pattern);
+    GEOSFree_r(ctxt_, pattern);
+}
 
 
 } // namespace tut
diff --git a/tests/unit/capi/GEOSRelatePatternTest.cpp b/tests/unit/capi/GEOSRelatePatternTest.cpp
index cfd1bfe19..d36002432 100644
--- a/tests/unit/capi/GEOSRelatePatternTest.cpp
+++ b/tests/unit/capi/GEOSRelatePatternTest.cpp
@@ -35,14 +35,23 @@ template<>
 template<>
 void object::test<2>()
 {
+    set_test_name("GEOSRelatePattern with automatic linearization");
+
+    useContext();
+
     geom1_ = fromWKT("CIRCULARSTRING (0 0, 1 1, 2 0)");
     geom2_ = fromWKT("LINESTRING (1 0, 2 1)");
 
     ensure(geom1_);
     ensure(geom2_);
 
-    ensure_equals("curved geometry not supported", GEOSRelatePattern(geom1_, geom2_, "0********"), 2);
-    ensure_equals("curved geometry not supported", GEOSRelatePattern(geom2_, geom1_, "0********"), 2);
+    ensure_equals(GEOSRelatePattern_r(ctxt_, geom1_, geom2_, "0********"), 2);
+    ensure_equals(GEOSRelatePattern_r(ctxt_, geom2_, geom1_, "0********"), 2);
+
+    useCurveConversion();
+
+    ensure_equals(GEOSRelatePattern_r(ctxt_, geom1_, geom2_, "0********"), 1);
+    ensure_equals(GEOSRelatePattern_r(ctxt_, geom2_, geom1_, "0********"), 1);
 }
 
 // invalid DE-9IM
diff --git a/tests/unit/capi/GEOSRelateTest.cpp b/tests/unit/capi/GEOSRelateTest.cpp
index f531ee4ab..ff03684ae 100644
--- a/tests/unit/capi/GEOSRelateTest.cpp
+++ b/tests/unit/capi/GEOSRelateTest.cpp
@@ -34,14 +34,24 @@ template<>
 template<>
 void object::test<2>()
 {
+    set_test_name("GEOSRelate with automatic linearization");
+
+    useContext();
+
     geom1_ = fromWKT("CIRCULARSTRING (0 0, 1 1, 2 0)");
     geom2_ = fromWKT("LINESTRING (1 0, 2 1)");
 
     ensure(geom1_);
     ensure(geom2_);
 
-    ensure("curved geometry not supported", GEOSRelate(geom1_, geom2_) == nullptr);
-    ensure("curved geometry not supported", GEOSRelate(geom2_, geom1_) == nullptr);
+    ensure(GEOSRelate_r(ctxt_, geom1_, geom2_) == nullptr);
+    ensure(GEOSRelate_r(ctxt_, geom2_, geom1_) == nullptr);
+
+    useCurveConversion();
+
+    char* pattern = GEOSRelate_r(ctxt_, geom1_, geom2_);
+    ensure(pattern);
+    GEOSFree_r(ctxt_, pattern);
 }
 
 } // namespace tut
diff --git a/tests/unit/capi/GEOSSharedPathsTest.cpp b/tests/unit/capi/GEOSSharedPathsTest.cpp
index 4316500ed..de71641ce 100644
--- a/tests/unit/capi/GEOSSharedPathsTest.cpp
+++ b/tests/unit/capi/GEOSSharedPathsTest.cpp
@@ -89,14 +89,25 @@ template<>
 template<>
 void object::test<4>()
 {
+    set_test_name("curved inputs");
+    useContext();
+
     geom1_ = fromWKT("CIRCULARSTRING (0 0, 1 1, 2 0)");
-    geom2_ = fromWKT("LINESTRING (1 0, 2 1)");
+    geom2_ = fromWKT("COMPOUNDCURVE ((4 0, 2 0), CIRCULARSTRING (2 0, 1 1, 0 0))");
 
     ensure(geom1_);
     ensure(geom2_);
 
-    result_ = GEOSSharedPaths(geom1_, geom2_);
+    result_ = GEOSSharedPaths_r(ctxt_, geom1_, geom2_);
     ensure("curved geometry not supported", result_ == nullptr);
+
+    useCurveConversion();
+
+    result_ = GEOSSharedPaths_r(ctxt_, geom1_, geom2_);
+    ensure(result_);
+
+    // result remains linear because segments are represented as
+    // a MultiLineString, not a LineString.
 }
 
 template<>
diff --git a/tests/unit/capi/GEOSSubdivideByGridTest.cpp b/tests/unit/capi/GEOSSubdivideByGridTest.cpp
index fc4398fb6..8d0b9c264 100644
--- a/tests/unit/capi/GEOSSubdivideByGridTest.cpp
+++ b/tests/unit/capi/GEOSSubdivideByGridTest.cpp
@@ -53,4 +53,27 @@ void object::test<2>()
     ensure_geometry_equals(expected_, result_);
 }
 
+template<>
+template<>
+void object::test<3>()
+{
+    set_test_name("curved inputs");
+    useContext();
+
+    input_ = fromWKT("CURVEPOLYGON (COMPOUNDCURVE(CIRCULARSTRING(0 0, 1 1, 2 0), (2 0, 0 0)))");
+    ensure(input_);
+
+    result_ = GEOSSubdivideByGrid_r(ctxt_, input_, 0, 0, 2, 1, 2, 1, false);
+    ensure(!result_);
+
+    useCurveConversion();
+    result_ = GEOSSubdivideByGrid_r(ctxt_, input_, 0, 0, 2, 1, 2, 1, false);
+    ensure(result_);
+
+    ensure_equals(GEOSGeomTypeId_r(ctxt_, result_), GEOS_GEOMETRYCOLLECTION);
+    ensure_equals(GEOSGetNumGeometries_r(ctxt_, result_), 2);
+    ensure_equals(GEOSGeomTypeId_r(ctxt_, GEOSGetGeometryN_r(ctxt_, result_, 0)), GEOS_CURVEPOLYGON);
+    ensure_equals(GEOSGeomTypeId_r(ctxt_, GEOSGetGeometryN_r(ctxt_, result_, 1)), GEOS_CURVEPOLYGON);
+}
+
 }
\ No newline at end of file
diff --git a/tests/unit/capi/GEOSTouchesTest.cpp b/tests/unit/capi/GEOSTouchesTest.cpp
index df6722d5f..a06c9ad0b 100644
--- a/tests/unit/capi/GEOSTouchesTest.cpp
+++ b/tests/unit/capi/GEOSTouchesTest.cpp
@@ -49,5 +49,22 @@ void object::test<2>()
     ensure_equals("curved geometry not supported", GEOSTouches(geom2_, geom1_), 2);
 }
 
+template<>
+template<>
+void object::test<3>()
+{
+    set_test_name("GEOSTouches with automatic linearization");
+
+    useContext();
+
+    geom1_ = fromWKT("CIRCULARSTRING (0 0, 1 1, 2 0)");
+    geom2_ = fromWKT("CIRCULARSTRING (2 0, 3 -1, 4 0)");
+
+    ensure_equals(2, GEOSTouches_r(ctxt_, geom1_, geom2_));
+
+    useCurveConversion();
+    ensure_equals(1, GEOSTouches_r(ctxt_, geom1_, geom2_));
+}
+
 } // namespace tut
 
diff --git a/tests/unit/capi/GEOSUnaryUnionTest.cpp b/tests/unit/capi/GEOSUnaryUnionTest.cpp
index ad431e12b..c4abb8458 100644
--- a/tests/unit/capi/GEOSUnaryUnionTest.cpp
+++ b/tests/unit/capi/GEOSUnaryUnionTest.cpp
@@ -219,12 +219,37 @@ template<>
 template<>
 void object::test<12>()
 {
-    input_ = fromWKT("CIRCULARSTRING (0 0, 1 1, 2 0)");
+    set_test_name("curved inputs");
+    useContext();
 
+    input_ = fromWKT("MULTICURVE (CIRCULARSTRING (-5 0, 0 5, 5 0), (-5 3, 5 3))");
     ensure(input_);
+    GEOSSetSRID_r(ctxt_, input_, 4326);
+    double input_length = -1;
+    ensure(GEOSLength_r(ctxt_, input_, &input_length));
 
-    result_ = GEOSUnaryUnion(input_);
-    ensure("curved geometry not supported", result_ == nullptr);
+    result_ = GEOSUnaryUnion_r(ctxt_, input_);
+    ensure(result_ == nullptr);
+
+    GEOSCurveToLineParams_setTolerance_r(ctxt_, curveToLineParams_, GEOS_CURVETOLINE_STEP_DEGREES, 1);
+    GEOSContext_setCurveToLineParams_r(ctxt_, curveToLineParams_);
+
+    // Input converted to line, output not converted to curve
+    result_ = GEOSUnaryUnion_r(ctxt_, input_);
+    ensure(result_);
+    ensure_equals(GEOSGeomTypeId_r(ctxt_, result_), GEOS_MULTILINESTRING);
+    ensure_equals(GEOSGetSRID_r(ctxt_, result_), 4326);
+    GEOSGeom_destroy_r(ctxt_, result_);
+
+    // Input converted to line, output converted to curve
+    GEOSContext_setLineToCurveParams_r(ctxt_, lineToCurveParams_);
+    result_ = GEOSUnaryUnion_r(ctxt_, input_);
+    ensure_equals(GEOSGeomTypeId_r(ctxt_, result_), GEOS_MULTICURVE);
+    double result_length = -1;
+    ensure(GEOSLength_r(ctxt_, result_, &result_length));
+
+    ensure_equals(GEOSGetSRID_r(ctxt_, result_), 4326);
+    ensure_equals("length does not match", result_length, input_length, 1e-5);
 }
 
 
diff --git a/tests/unit/capi/GEOSUnionTest.cpp b/tests/unit/capi/GEOSUnionTest.cpp
index 4c7e89e29..835aaaf0f 100644
--- a/tests/unit/capi/GEOSUnionTest.cpp
+++ b/tests/unit/capi/GEOSUnionTest.cpp
@@ -1,6 +1,7 @@
 #include <tut/tut.hpp>
 // geos
 #include <geos_c.h>
+#include <geos/constants.h>
 
 #include "capi_test_utils.h"
 
diff --git a/tests/unit/capi/GEOSVoronoiDiagramTest.cpp b/tests/unit/capi/GEOSVoronoiDiagramTest.cpp
index d79181351..a7785ec01 100644
--- a/tests/unit/capi/GEOSVoronoiDiagramTest.cpp
+++ b/tests/unit/capi/GEOSVoronoiDiagramTest.cpp
@@ -183,11 +183,13 @@ template<>
 void object::test<9>
 ()
 {
+    set_test_name("curved inputs");
+
     input_ = fromWKT("CIRCULARSTRING (0 0, 1 1, 2 0)");
     ensure(input_);
 
     result_ = GEOSVoronoiDiagram(input_, nullptr, 0, 0);
-    ensure("curved geometry not supported", result_ == nullptr);
+    ensure(result_);
 }
 
 } // namespace tut
diff --git a/tests/unit/capi/GEOSWithinTest.cpp b/tests/unit/capi/GEOSWithinTest.cpp
index 387f40909..42def3e7f 100644
--- a/tests/unit/capi/GEOSWithinTest.cpp
+++ b/tests/unit/capi/GEOSWithinTest.cpp
@@ -88,13 +88,19 @@ template<>
 template<>
 void object::test<4>()
 {
-    geom1_ = fromWKT("CIRCULARSTRING (0 0, 1 1, 2 0)");
-    geom2_ = fromWKT("LINESTRING (1 0, 2 0)");
+    set_test_name("GEOSWithin with automatic linearization");
+    useContext();
+
+    geom1_ = fromWKT("CURVEPOLYGON (COMPOUNDCURVE (CIRCULARSTRING (0 0, 1 1, 2 0), (2 0, 0 0)))");
+    geom2_ = fromWKT("LINESTRING (1 0.5, 1 0.6)");
 
     ensure(geom1_);
     ensure(geom2_);
 
-    ensure_equals("curved geometry not supported", GEOSWithin(geom2_, geom1_), 2);
+    ensure_equals(GEOSWithin_r(ctxt_, geom2_, geom1_), 2);
+    useCurveConversion();
+    ensure_equals(GEOSWithin_r(ctxt_, geom1_, geom2_), 0);
+    ensure_equals(GEOSWithin_r(ctxt_, geom2_, geom1_), 1);
 }
 
 } // namespace tut
diff --git a/tests/unit/capi/GEOSisRingTest.cpp b/tests/unit/capi/GEOSisRingTest.cpp
index 0f263be6c..25a9c31fd 100644
--- a/tests/unit/capi/GEOSisRingTest.cpp
+++ b/tests/unit/capi/GEOSisRingTest.cpp
@@ -80,11 +80,15 @@ template<>
 void object::test<6>
 ()
 {
-    geom1_ = GEOSGeomFromWKT("COMPOUNDCURVE (CIRCULARSTRING (0 0, 1 1, 2 0), (2 0, 0 0))");
+    set_test_name("curved inputs");
+    useContext();
+
+    geom1_ = fromWKT("COMPOUNDCURVE (CIRCULARSTRING (0 0, 1 1, 2 0), (2 0, 0 0))");
     ensure(geom1_);
 
-    char r = GEOSisRing(geom1_);
-    ensure_equals("curved geometetries not supported", r, 2);
+    //ensure_equals(GEOSisRing_r(ctxt_, geom1_), 2);
+    useCurveConversion();
+    ensure_equals(GEOSisRing_r(ctxt_, geom1_), 1);
 }
 
 } // namespace tut
diff --git a/tests/unit/capi/GEOSisSimpleDetailTest.cpp b/tests/unit/capi/GEOSisSimpleDetailTest.cpp
index 95ce75675..f78a42069 100644
--- a/tests/unit/capi/GEOSisSimpleDetailTest.cpp
+++ b/tests/unit/capi/GEOSisSimpleDetailTest.cpp
@@ -61,12 +61,15 @@ template<>
 template<>
 void object::test<4>()
 {
-    set_test_name("error raised on curved geometry");
+    set_test_name("curved inputs");
+    useContext();
 
     input_ = fromWKT("CIRCULARSTRING (0 0, 1 1, 2 0)");
     ensure(input_ != nullptr);
 
-    ensure_equals(GEOSisSimpleDetail(input_, 0, &result_), 2);
+    ensure_equals(GEOSisSimpleDetail_r(ctxt_, input_, 0, &result_), 2);
+    useCurveConversion();
+    ensure_equals(GEOSisSimpleDetail_r(ctxt_, input_, 0, &result_), 1);
 }
 
 template<>
diff --git a/tests/unit/capi/GEOSisSimpleTest.cpp b/tests/unit/capi/GEOSisSimpleTest.cpp
index 762bacdb8..9ca2d0928 100644
--- a/tests/unit/capi/GEOSisSimpleTest.cpp
+++ b/tests/unit/capi/GEOSisSimpleTest.cpp
@@ -36,11 +36,15 @@ template<>
 template<>
 void object::test<3>()
 {
+    set_test_name("curved inputs");
+    useContext();
+
     input_ = fromWKT("CIRCULARSTRING (0 0, 1 1, 2 0)");
     ensure(input_ != nullptr);
 
-    char ret = GEOSisSimple(input_);
-    ensure_equals("error raised on curved geometry", ret, 2);
+    ensure_equals(GEOSisSimple_r(ctxt_, input_), 2);
+    useCurveConversion();
+    ensure_equals(GEOSisSimple_r(ctxt_, input_), 1);
 }
 
 } // namespace tut
diff --git a/tests/unit/capi/GEOSisValidDetailTest.cpp b/tests/unit/capi/GEOSisValidDetailTest.cpp
index a4f8e3a38..935b37f8d 100644
--- a/tests/unit/capi/GEOSisValidDetailTest.cpp
+++ b/tests/unit/capi/GEOSisValidDetailTest.cpp
@@ -37,7 +37,9 @@ struct test_capiisvaliddetail_data : public capitest::utility {
     ~test_capiisvaliddetail_data()
     {
         GEOSGeom_destroy(loc_);
-        GEOSFree(reason_);
+        if (reason_) {
+            GEOSFree(reason_);
+        }
     }
 
 };
@@ -144,11 +146,13 @@ template<>
 template<>
 void object::test<7>()
 {
+    useContext();
     input_ = fromWKT("CURVEPOLYGON (COMPOUNDCURVE( CIRCULARSTRING (0 0, 1 1, 2 0), (2 0, 0 0)))");
     ensure(input_ != nullptr);
 
-    char ret = GEOSisValidDetail(input_, 0, nullptr, nullptr);
-    ensure_equals("error raised on curved geometry", ret, 2);
+    ensure_equals(GEOSisValidDetail_r(ctxt_, input_, 0, nullptr, nullptr), 2);
+    useCurveConversion();
+    ensure_equals(GEOSisValidDetail_r(ctxt_, input_, 0, nullptr, nullptr), 1);
 }
 
 } // namespace tut
diff --git a/tests/unit/capi/GEOSisValidReasonTest.cpp b/tests/unit/capi/GEOSisValidReasonTest.cpp
index e3146c443..3ad425952 100644
--- a/tests/unit/capi/GEOSisValidReasonTest.cpp
+++ b/tests/unit/capi/GEOSisValidReasonTest.cpp
@@ -61,11 +61,22 @@ template<>
 template<>
 void object::test<7>()
 {
-    input_ = fromWKT("CURVEPOLYGON (COMPOUNDCURVE( CIRCULARSTRING (0 0, 1 1, 2 0), (2 0, 0 0)))");
+    set_test_name("GEOSIsValidReason with automatic linearization");
+
+    useContext();
+
+    input_ = fromWKT("CURVEPOLYGON (COMPOUNDCURVE( CIRCULARSTRING (0 0, 1 1, 2 0), (2 0, 1 1, 0 0)))");
     ensure(input_ != nullptr);
 
-    char* reason = GEOSisValidReason(input_);
+    char* reason = GEOSisValidReason_r(ctxt_, input_);
     ensure(reason == nullptr);
+
+    useCurveConversion();
+
+    reason = GEOSisValidReason_r(ctxt_, input_);
+    ensure(reason != nullptr);
+    ensure_equals(std::string(reason).substr(0, 17), "Self-intersection");
+    GEOSFree_r(ctxt_, reason);
 }
 
 
diff --git a/tests/unit/capi/GEOSisValidTest.cpp b/tests/unit/capi/GEOSisValidTest.cpp
index 046a53feb..4f59ca80c 100644
--- a/tests/unit/capi/GEOSisValidTest.cpp
+++ b/tests/unit/capi/GEOSisValidTest.cpp
@@ -68,11 +68,15 @@ template<>
 template<>
 void object::test<5>()
 {
+    set_test_name("GEOSIsValid with automatic linearization");
+    useContext();
+
     input_ = fromWKT("CURVEPOLYGON (COMPOUNDCURVE( CIRCULARSTRING (0 0, 1 1, 2 0), (2 0, 0 0)))");
     ensure(input_ != nullptr);
 
-    char ret = GEOSisValid(input_);
-    ensure_equals("error raised on curved geometry", ret, 2);
+    ensure_equals(GEOSisValid_r(ctxt_, input_), 2);
+    useCurveConversion();
+    ensure_equals(GEOSisValid_r(ctxt_, input_), 1);
 }
 
 } // namespace tut
diff --git a/tests/unit/capi/capi_test_utils.h b/tests/unit/capi/capi_test_utils.h
index 7cc711ed7..a9680cf1b 100644
--- a/tests/unit/capi/capi_test_utils.h
+++ b/tests/unit/capi/capi_test_utils.h
@@ -24,7 +24,11 @@ namespace capitest {
         GEOSGeometry* result_ = nullptr;
         GEOSGeometry* expected_ = nullptr;
         char* wkt_ = nullptr;
-       char* str_ = nullptr;
+        char* str_ = nullptr;
+
+        GEOSContextHandle_t ctxt_ = nullptr;
+        GEOSCurveToLineParams* curveToLineParams_ = nullptr;
+        GEOSLineToCurveParams* lineToCurveParams_ = nullptr;
 
         utility()
         {
@@ -32,6 +36,9 @@ namespace capitest {
             wktw_ = GEOSWKTWriter_create();
             GEOSWKTWriter_setRoundingPrecision(wktw_, 10);
 
+            curveToLineParams_ = GEOSCurveToLineParams_create();
+            lineToCurveParams_ = GEOSLineToCurveParams_create();
+
 #ifdef HAVE_FENV
             std::feclearexcept(FE_ALL_EXCEPT);
 #endif
@@ -39,18 +46,53 @@ namespace capitest {
 
         virtual ~utility()
         {
-            if (wktw_)     GEOSWKTWriter_destroy(wktw_);
-            if (geom1_)    GEOSGeom_destroy(geom1_);
-            if (geom2_)    GEOSGeom_destroy(geom2_);
-            if (geom3_)    GEOSGeom_destroy(geom3_);
-            if (input_)    GEOSGeom_destroy(input_);
-            if (result_)   GEOSGeom_destroy(result_);
-            if (expected_) GEOSGeom_destroy(expected_);
-            if (wkt_)      GEOSFree(wkt_);
-            if (str_)      GEOSFree(str_);
-            finishGEOS();
+            if (ctxt_ == nullptr) {
+                if (wktw_)     GEOSWKTWriter_destroy(wktw_);
+                if (geom1_)    GEOSGeom_destroy(geom1_);
+                if (geom2_)    GEOSGeom_destroy(geom2_);
+                if (geom3_)    GEOSGeom_destroy(geom3_);
+                if (input_)    GEOSGeom_destroy(input_);
+                if (result_)   GEOSGeom_destroy(result_);
+                if (expected_) GEOSGeom_destroy(expected_);
+                if (wkt_)      GEOSFree(wkt_);
+                if (str_)      GEOSFree(str_);
+                if (lineToCurveParams_) GEOSLineToCurveParams_destroy(lineToCurveParams_);
+                if (curveToLineParams_) GEOSCurveToLineParams_destroy(curveToLineParams_);
+                finishGEOS();
+            } else {
+                if (wktw_)     GEOSWKTWriter_destroy_r(ctxt_, wktw_);
+                if (geom1_)    GEOSGeom_destroy_r(ctxt_, geom1_);
+                if (geom2_)    GEOSGeom_destroy_r(ctxt_, geom2_);
+                if (geom3_)    GEOSGeom_destroy_r(ctxt_, geom3_);
+                if (input_)    GEOSGeom_destroy_r(ctxt_, input_);
+                if (result_)   GEOSGeom_destroy_r(ctxt_, result_);
+                if (expected_) GEOSGeom_destroy_r(ctxt_, expected_);
+                if (wkt_)      GEOSFree_r(ctxt_, wkt_);
+                if (str_)      GEOSFree_r(ctxt_, str_);
+                if (lineToCurveParams_) GEOSLineToCurveParams_destroy(lineToCurveParams_);
+                if (curveToLineParams_) GEOSCurveToLineParams_destroy(curveToLineParams_);
+                finishGEOS_r(ctxt_);
+            }
         }
 
+        // Use an explicit context handle (stored as ctxt_) instead of the default
+        // context handle.
+        void useContext() {
+            finishGEOS();
+            ctxt_ = initGEOS_r(notice, notice);
+            GEOSWKTWriter_setRoundingPrecision(wktw_, 10);
+        }
+
+        void useCurveConversion() {
+            GEOSCurveToLineParams* curveToLineParams = GEOSCurveToLineParams_create();
+            GEOSLineToCurveParams* lineToCurveParams = GEOSLineToCurveParams_create();
+
+            GEOSContext_setCurveToLineParams_r(ctxt_, curveToLineParams);
+            GEOSContext_setLineToCurveParams_r(ctxt_, lineToCurveParams);
+
+            GEOSCurveToLineParams_destroy(curveToLineParams);
+            GEOSLineToCurveParams_destroy(lineToCurveParams);
+        }
 
         static void notice(GEOS_PRINTF_FORMAT const char* fmt, ...) GEOS_PRINTF_FORMAT_ATTR(1, 2)
         {
@@ -74,7 +116,7 @@ namespace capitest {
         GEOSGeometry*
         fromWKT(const char* wkt)
         {
-            GEOSGeometry* g = GEOSGeomFromWKT(wkt);
+            GEOSGeometry* g = ctxt_ ? GEOSGeomFromWKT_r(ctxt_, wkt) : GEOSGeomFromWKT(wkt);
             if (g == nullptr) {
                 std::string message = "WKT is invalid: " + std::string(wkt);
                 tut::ensure(message, g != nullptr);
@@ -85,9 +127,19 @@ namespace capitest {
         std::string
         toWKT(const GEOSGeometry* g)
         {
-            char* wkt = GEOSWKTWriter_write(wktw_, g);
-            std::string ret(wkt);
-            GEOSFree(wkt);
+            char* wkt;
+            std::string ret;
+
+            if (ctxt_ == nullptr) {
+                wkt = GEOSWKTWriter_write(wktw_, g);
+                ret = wkt;
+                GEOSFree(wkt);
+            } else {
+                wkt = GEOSWKTWriter_write_r(ctxt_, wktw_, g);
+                ret = wkt;
+                GEOSFree_r(ctxt_, wkt);
+            }
+
             return ret;
         }
 
@@ -97,6 +149,10 @@ namespace capitest {
             char rslt;
             if (g1 == nullptr || g2 == nullptr) {
                 rslt = (g1 == nullptr && g2 == nullptr) ? 1 : 0;
+            } else if (ctxt_ != nullptr) {
+                GEOSNormalize_r(ctxt_, g1);
+                GEOSNormalize_r(ctxt_, g2);
+                rslt = GEOSEqualsExact_r(ctxt_, g1, g2, tolerance);
             }
             else {
                 GEOSNormalize(g1);
@@ -114,6 +170,9 @@ namespace capitest {
             if (g1 == nullptr || g2 == nullptr) {
                 rslt = (g1 == nullptr && g2 == nullptr) ? 1 : 0;
             }
+            else if (ctxt_) {
+                rslt = GEOSEqualsExact_r(ctxt_, g1, g2, tolerance);
+            }
             else {
                 rslt = GEOSEqualsExact(g1, g2, tolerance);
             }
@@ -127,8 +186,9 @@ namespace capitest {
             char rslt;
             if (g1 == nullptr || g2 == nullptr) {
                 rslt = (g1 == nullptr && g2 == nullptr) ? 1 : 0;
-            }
-            else {
+            } else if (ctxt_) {
+                rslt = GEOSEqualsIdentical_r(ctxt_, g1, g2);
+            } else {
                 rslt = GEOSEqualsIdentical(g1, g2);
             }
             report_not_equal("ensure_equals_identical", g1, g2, 1e-12, rslt);
@@ -167,17 +227,9 @@ namespace capitest {
             if (rslt == 1) return;
             //TODO: handle rslt exception value
 
-            char* wkt1 = nullptr;
-            char* wkt2 = nullptr;
-            if (g1 != nullptr)
-                wkt1 = GEOSWKTWriter_write(wktw_, g1);
-            if (g2 != nullptr)
-                wkt2 = GEOSWKTWriter_write(wktw_, g2);
-            const char* val1 = (g1 == nullptr) ? "null" : wkt1;
-            const char* val2 = (g2 == nullptr) ? "null" : wkt2;
-            std::fprintf(stdout, "\n%s : %s != %s (tol = %f)\n", tag, val1, val2, tolerance);
-            GEOSFree(wkt1);
-            GEOSFree(wkt2);
+            const std::string wkt1 = g1 == nullptr ? "null" : toWKT(g1);
+            const std::string wkt2 = g2 == nullptr ? "null" : toWKT(g2);
+            std::fprintf(stdout, "\n%s : %s != %s (tol = %f)\n", tag, wkt1.c_str(), wkt2.c_str(), tolerance);
         }
 
     };

commit 1d65863b4972dd8050f2fa2aba73a5618775c305
Author: Daniel Baston <dbaston at gmail.com>
Date:   Thu Feb 12 12:58:34 2026 -0500

    C API: Add GEOSCurveToLine, GEOSLineToCurve

diff --git a/capi/geos_c.cpp b/capi/geos_c.cpp
index 477fd0fcf..a664d9e21 100644
--- a/capi/geos_c.cpp
+++ b/capi/geos_c.cpp
@@ -15,6 +15,8 @@
  *
  ***********************************************************************/
 
+#include <geos/algorithm/CurveToLineParams.h>
+#include <geos/algorithm/LineToCurveParams.h>
 #include <geos/geom/prep/PreparedGeometryFactory.h>
 #include <geos/index/strtree/TemplateSTRtree.h>
 #include <geos/io/WKTReader.h>
@@ -43,7 +45,9 @@
 #define GEOSPreparedGeometry geos::geom::prep::PreparedGeometry
 #define GEOSClusterInfo geos::operation::cluster::Clusters
 #define GEOSCoordSequence geos::geom::CoordinateSequence
+#define GEOSCurveToLineParams geos::algorithm::CurveToLineParams
 #define GEOSBufferParams geos::operation::buffer::BufferParameters
+#define GEOSLineToCurveParams geos::algorithm::LineToCurveParams
 #define GEOSSTRtree geos::index::strtree::TemplateSTRtree<void*>
 #define GEOSWKTReader geos::io::WKTReader
 #define GEOSWKTWriter geos::io::WKTWriter
@@ -217,6 +221,65 @@ extern "C" {
         return GEOSCoveredBy_r(handle, g1, g2);
     }
 
+    GEOSCurveToLineParams*
+    GEOSCurveToLineParams_create()
+    {
+        return new geos::algorithm::CurveToLineParams;
+    }
+
+    int
+    GEOSCurveToLineParams_setTolerance(GEOSCurveToLineParams* params, int toleranceType, double toleranceValue)
+    {
+        return GEOSCurveToLineParams_setTolerance_r(handle, params, toleranceType, toleranceValue);
+    }
+
+    void
+    GEOSCurveToLineParams_destroy(GEOSCurveToLineParams* params)
+    {
+        delete params;
+    }
+
+    GEOSGeometry*
+    GEOSCurveToLine(const GEOSGeometry* g, const GEOSCurveToLineParams* params)
+    {
+        return GEOSCurveToLine_r(handle, g, params);
+    }
+
+    GEOSGeometry*
+    GEOSLineToCurve(const GEOSGeometry* g, const GEOSLineToCurveParams* params)
+    {
+        return GEOSLineToCurve_r(handle, g, params);
+    }
+
+    GEOSLineToCurveParams*
+    GEOSLineToCurveParams_create()
+    {
+        return new geos::algorithm::LineToCurveParams;
+    }
+
+    void
+    GEOSLineToCurveParams_destroy(GEOSLineToCurveParams* params)
+    {
+        delete params;
+    }
+
+    int
+    GEOSLineToCurveParams_setRadiusTolerance(GEOSLineToCurveParams* params, double tolerance)
+    {
+        return GEOSLineToCurveParams_setRadiusTolerance_r(handle, params, tolerance);
+    }
+
+    int
+    GEOSLineToCurveParams_setMaxStepDegrees(GEOSLineToCurveParams* params, double tolerance)
+    {
+        return GEOSLineToCurveParams_setMaxStepDegrees_r(handle, params, tolerance);
+    }
+
+    int
+    GEOSLineToCurveParams_setMaxAngleDifferenceDegrees(GEOSLineToCurveParams* params, double tolerance)
+    {
+        return GEOSLineToCurveParams_setMaxAngleDifferenceDegrees_r(handle, params, tolerance);
+    }
 
 //-------------------------------------------------------------------
 // low-level relate functions
diff --git a/capi/geos_c.h.in b/capi/geos_c.h.in
index 855ef4d36..020769d9d 100644
--- a/capi/geos_c.h.in
+++ b/capi/geos_c.h.in
@@ -188,6 +188,20 @@ typedef struct GEOSMakeValidParams_t GEOSMakeValidParams;
 */
 typedef struct GEOSClusterInfo_t GEOSClusterInfo;
 
+/**
+* Parameter object for curve-to-line conversion
+* \see GEOSCurveToLineParams_create()
+* \see GEOSCurveToLineParams_destroy()
+*/
+typedef struct GEOSCurveToLineParams_t GEOSCurveToLineParams;
+
+/**
+* Parameter object for line-to-curve conversion
+* \see GEOSLineToCurveParams_create()
+* \see GEOSLineToCurveParams_destroy()
+*/
+typedef struct GEOSLineToCurveParams_t GEOSLineToCurveParams;
+
 #endif
 
 /** \cond */
@@ -1818,6 +1832,53 @@ extern double GEOS_DLL GEOSGeom_getPrecision_r(
     GEOSContextHandle_t handle,
     const GEOSGeometry *g);
 
+/**
+* Curve-to-line tolerance types.
+*/
+enum GEOSCurveToLineTolerance {
+    /** Maximum angle between successive vertices of a linearized arc */
+    GEOS_CURVETOLINE_STEP_DEGREES = 0,
+    /** Maximum distance between an arc and its linearized approximation */
+    GEOS_CURVETOLINE_MAX_DEVIATION = 1
+};
+
+/** \see GEOSCurveToLine */
+extern GEOSGeometry GEOS_DLL *GEOSCurveToLine_r(
+    GEOSContextHandle_t handle,
+    const GEOSGeometry* g,
+    const GEOSCurveToLineParams* params);
+
+/** \see GEOSCurveToLineParams_setTolerance */
+extern int GEOS_DLL GEOSCurveToLineParams_setTolerance_r(
+    GEOSContextHandle_t extHandle,
+    GEOSCurveToLineParams* params,
+    int toleranceType,
+    double toleranceValue);
+
+/** \see GEOSLineToCurve */
+extern GEOSGeometry GEOS_DLL *GEOSLineToCurve_r(
+    GEOSContextHandle_t handle,
+    const GEOSGeometry* g,
+    const GEOSLineToCurveParams* params);
+
+/** \see GEOSLineToCurveParams_setRadiusTolerance */
+extern int GEOS_DLL GEOSLineToCurveParams_setRadiusTolerance_r(
+        GEOSContextHandle_t extHandle,
+        GEOSLineToCurveParams* params,
+        double tolerance);
+
+/** \see GEOSLineToCurveParams_setMaxStepDegrees */
+extern int GEOS_DLL GEOSLineToCurveParams_setMaxStepDegrees_r(
+        GEOSContextHandle_t extHandle,
+        GEOSLineToCurveParams* params,
+        double tolerance);
+
+/** \see GEOSLineToCurveParams_setMaxAngleDifferenceDegrees */
+extern int GEOS_DLL GEOSLineToCurveParams_setMaxAngleDifferenceDegrees_r(
+        GEOSContextHandle_t extHandle,
+        GEOSLineToCurveParams* params,
+        double tolerance);
+
 /** \see GEOSGetNumInteriorRings */
 extern int GEOS_DLL GEOSGetNumInteriorRings_r(
     GEOSContextHandle_t handle,
@@ -5719,6 +5780,110 @@ extern GEOSGeometry GEOS_DLL *GEOSGeom_setPrecision(
 
 ///@}
 
+/* ============================================================== */
+
+/** @name Line/Curve conversion
+* Functions for converting between curved and linear geometry types.
+*/
+///@{
+
+/** Approximate all circular arcs in a geometry using line segments.
+*   If the input has no circular arcs, a copy will be returned.
+*   \param g the geometry to convert
+*   \param params parameters to use in the linearization.
+*   \return The linearized geometry.
+*           Caller must free with GEOSGeom_destroy()
+*           NULL on exception
+*
+*   \since 3.15
+*/ 
+extern GEOSGeometry GEOS_DLL *GEOSCurveToLine(
+    const GEOSGeometry* g,
+    const GEOSCurveToLineParams* params);
+
+extern GEOSCurveToLineParams GEOS_DLL *GEOSCurveToLineParams_create();
+
+extern void GEOS_DLL GEOSCurveToLineParams_destroy(GEOSCurveToLineParams* params);
+
+/** Set the tolerance type and value to use when converting curves to lines.
+*
+*   \param params parameters to use in the conversion.
+*   \param toleranceType \see GEOSCurveToLineTolerance
+*   \param toleranceValue the value to use with toleranceType
+*   \return 1 if the operation succeeded, 0 otherwise*
+*
+*   \since 3.15
+*/ 
+extern int GEOS_DLL GEOSCurveToLineParams_setTolerance(
+    GEOSCurveToLineParams* params,
+    int toleranceType,
+    double toleranceValue);
+
+/** Replace line segments in the geometry with circular arcs, where possible.
+*
+*   \param g the geometry to convert
+*   \param params parameters to use in the conversion.
+*   \return The converted geometry.
+*           Caller must free with GEOSGeom_destroy()
+*           NULL on exception
+*
+*   \since 3.15
+*/ 
+extern GEOSGeometry GEOS_DLL *GEOSLineToCurve(
+    const GEOSGeometry* g,
+    const GEOSLineToCurveParams* params);
+
+extern GEOSLineToCurveParams GEOS_DLL *GEOSLineToCurveParams_create();
+
+extern void GEOS_DLL GEOSLineToCurveParams_destroy(GEOSLineToCurveParams* params);
+
+/** Set the radius tolerance used in line-to-curve conversion.
+*
+*   A vertex may be considered to continue a circular arc if its distance to the
+*   previously-computed circle center is within the specified fraction of the
+*   circle radius.
+* 
+*   \param params the parameters object
+*   \param tolerance the tolerance value
+*   \return 1 on success, 0 on failure
+*   \since 3.15
+*/
+extern int GEOS_DLL GEOSLineToCurveParams_setRadiusTolerance(
+        GEOSLineToCurveParams* params,
+        double tolerance);
+
+/** Set the maximum angle spacing used in line-to-curve conversion.
+*
+*   A vertex may be considered to continue a circular arc if the circular sector
+*   between the vertex and the final point in the arc is less than the specified tolerance. Because of round-off errors, this value should be slightly greater than the one used
+*   in \ref GEOSCurveToLineParams_setTolerance.
+* 
+*   \param params the parameters object
+*   \param tolerance the tolerance value
+*   \return 1 on success, 0 on failure
+*   \since 3.15
+*/
+extern int GEOS_DLL GEOSLineToCurveParams_setMaxStepDegrees(
+        GEOSLineToCurveParams* params,
+        double tolerance);
+
+/** Set the maximum angle difference used in line-to-curve conversion
+*
+*   A vertex p[n] may be considered to continue a circular arc if the 
+*   difference between the angles p[n-2] / p[n-1] / p[n] is within the
+*   specified difference of the angle p[0] / p[1] / p[2].
+* 
+*   \param params the parameters object
+*   \param tolerance the tolerance value
+*   \return 1 on success, 0 on failure
+*   \since 3.15
+*/
+extern int GEOS_DLL GEOSLineToCurveParams_setMaxAngleDifferenceDegrees(
+        GEOSLineToCurveParams* params,
+        double tolerance);
+
+///@}
+
 /* ============================================================== */
 /** @name Spatial Predicates
 * Functions computing binary spatial predicates using the DE-9IM topology model.
diff --git a/capi/geos_ts_c.cpp b/capi/geos_ts_c.cpp
index e5f9e93d1..3cf883518 100644
--- a/capi/geos_ts_c.cpp
+++ b/capi/geos_ts_c.cpp
@@ -18,6 +18,8 @@
  ***********************************************************************/
 
 #include <geos/algorithm/BoundaryNodeRule.h>
+#include <geos/algorithm/CurveToLineParams.h>
+#include <geos/algorithm/LineToCurveParams.h>
 #include <geos/algorithm/MinimumBoundingCircle.h>
 #include <geos/algorithm/MinimumDiameter.h>
 #include <geos/algorithm/MinimumAreaRectangle.h>
@@ -148,7 +150,9 @@
 #define GEOSPreparedGeometry geos::geom::prep::PreparedGeometry
 #define GEOSClusterInfo geos::operation::cluster::Clusters
 #define GEOSCoordSequence geos::geom::CoordinateSequence
+#define GEOSCurveToLineParams geos::algorithm::CurveToLineParams
 #define GEOSBufferParams geos::operation::buffer::BufferParameters
+#define GEOSLineToCurveParams geos::algorithm::LineToCurveParams
 #define GEOSSTRtree geos::index::strtree::TemplateSTRtree<void*>
 #define GEOSWKTReader geos::io::WKTReader
 #define GEOSWKTWriter geos::io::WKTWriter
@@ -3589,6 +3593,61 @@ extern "C" {
         });
     }
 
+    GEOSGeometry*
+    GEOSCurveToLine_r(GEOSContextHandle_t extHandle, const Geometry *g,
+                      const GEOSCurveToLineParams* params)
+    {
+        return execute(extHandle, [&]() {
+            return g->getLinearized(*params).release();
+        });
+    }
+
+    int
+    GEOSCurveToLineParams_setTolerance_r(GEOSContextHandle_t extHandle, GEOSCurveToLineParams* params,
+                                         int toleranceType, double toleranceValue)
+    {
+        return execute(extHandle, 0, [&]() {
+            params->setTolerance(static_cast<GEOSCurveToLineParams::TOLERANCE_TYPE>(toleranceType), toleranceValue);
+            return 1;
+        });
+    }
+
+    GEOSGeometry*
+    GEOSLineToCurve_r(GEOSContextHandle_t extHandle, const GEOSGeometry* g,
+                      const GEOSLineToCurveParams* params)
+    {
+        return execute(extHandle, [&]() {
+            return g->getCurved(*params).release();
+        });
+    }
+
+    int
+    GEOSLineToCurveParams_setMaxAngleDifferenceDegrees_r(GEOSContextHandle_t extHandle, GEOSLineToCurveParams* params, double tolerance)
+    {
+        return execute(extHandle, 0, [&]() {
+            params->setMaxExteriorAngleDifferenceDegrees(tolerance);
+            return 1;
+        });
+    }
+
+    int
+    GEOSLineToCurveParams_setMaxStepDegrees_r(GEOSContextHandle_t extHandle, GEOSLineToCurveParams* params, double tolerance)
+    {
+        return execute(extHandle, 0, [&]() {
+            params->setMaxStepDegrees(tolerance);
+            return 1;
+        });
+    }
+
+    int
+    GEOSLineToCurveParams_setRadiusTolerance_r(GEOSContextHandle_t extHandle, GEOSLineToCurveParams* params, double tolerance)
+    {
+        return execute(extHandle, 0, [&]() {
+            params->setRadiusTolerance(tolerance);
+            return 1;
+        });
+    }
+
     int
     GEOSGeom_getDimensions_r(GEOSContextHandle_t extHandle, const Geometry* g)
     {
diff --git a/tests/unit/capi/GEOSCurveToLineTest.cpp b/tests/unit/capi/GEOSCurveToLineTest.cpp
new file mode 100644
index 000000000..a708d3526
--- /dev/null
+++ b/tests/unit/capi/GEOSCurveToLineTest.cpp
@@ -0,0 +1,109 @@
+#include <tut/tut.hpp>
+// geos
+#include <geos_c.h>
+// std
+#include <cstring>
+
+#include "capi_test_utils.h"
+
+namespace tut {
+
+//
+// Test Group
+//
+struct test_capigeoscurvetoline_data : public capitest::utility {
+    test_capigeoscurvetoline_data() {
+        params_ = GEOSCurveToLineParams_create();
+    }
+
+    ~test_capigeoscurvetoline_data() override {
+        GEOSCurveToLineParams_destroy(params_);
+    }
+
+    GEOSCurveToLineParams* params_;
+};
+
+using group = test_group<test_capigeoscurvetoline_data>;
+using object = group::object;
+
+group test_capigeoscurvetoline_group("capi::GEOSCurveToLine");
+
+//
+// Test Cases
+//
+
+template<>
+template<>
+void object::test<1>()
+{
+    set_test_name("empty input converted to linear type");
+
+    input_ = fromWKT("CIRCULARSTRING EMPTY");
+    result_ = GEOSCurveToLine(input_, params_);
+    ensure_equals(GEOSGeomTypeId(result_), GEOS_LINESTRING);
+    ensure(GEOSisEmpty(result_));
+}
+
+template<>
+template<>
+void object::test<2>()
+{
+    set_test_name("linear input copied");
+
+    input_ = fromWKT("LINESTRING (0 0, 1 1, 2 0)");
+    result_ = GEOSCurveToLine(input_, params_);
+
+    ensure_geometry_equals_identical(result_, input_);
+}
+
+template<>
+template<>
+void object::test<3>()
+{
+    set_test_name("CircularString, step size degrees");
+
+    input_ = fromWKT("CIRCULARSTRING (0 0, 100 100, 200 0)");
+
+    ensure(GEOSCurveToLineParams_setTolerance(params_, GEOS_CURVETOLINE_STEP_DEGREES, 30));
+    result_ = GEOSCurveToLine(input_, params_);
+    expected_ = fromWKT("LINESTRING(0 0,13.3975 50,50 86.6025,100 100,150 86.6025,186.6025 50,200 0)");
+
+    ensure_geometry_equals_exact(result_, expected_, 1e-3);
+}
+
+template<>
+template<>
+void object::test<4>()
+{
+    set_test_name("CircularString, maximum deviation");
+
+    input_ = fromWKT("CIRCULARSTRING (0 0, 100 100, 200 0)");
+    ensure(GEOSCurveToLineParams_setTolerance(params_, GEOS_CURVETOLINE_MAX_DEVIATION, 10));
+    result_ = GEOSCurveToLine(input_, params_);
+    expected_ = fromWKT("LINESTRING(0 0,30 70,100 100,170 70,200 0)");
+
+    ensure_geometry_equals_exact(result_, expected_, 1.4);
+}
+
+template<>
+template<>
+void object::test<5>()
+{
+    set_test_name("invalid tolerance type");
+
+    input_ = fromWKT("CIRCULARSTRING (0 0, 1 1, 2 0)");
+    ensure(!GEOSCurveToLineParams_setTolerance(params_, -3, 10));
+}
+
+template<>
+template<>
+void object::test<6>()
+{
+    set_test_name("invalid tolerance value");
+
+    input_ = fromWKT("CIRCULARSTRING (0 0, 1 1, 2 0)");
+    ensure(!GEOSCurveToLineParams_setTolerance(params_, GEOS_CURVETOLINE_STEP_DEGREES, -1));
+    ensure(!GEOSCurveToLineParams_setTolerance(params_, GEOS_CURVETOLINE_STEP_DEGREES, std::numeric_limits<double>::quiet_NaN()));
+}
+
+} // namespace tut
\ No newline at end of file
diff --git a/tests/unit/capi/GEOSLineToCurveTest.cpp b/tests/unit/capi/GEOSLineToCurveTest.cpp
new file mode 100644
index 000000000..55fdaa0df
--- /dev/null
+++ b/tests/unit/capi/GEOSLineToCurveTest.cpp
@@ -0,0 +1,89 @@
+#include <tut/tut.hpp>
+// geos
+#include <geos_c.h>
+// std
+#include <cstring>
+
+#include "capi_test_utils.h"
+
+namespace tut {
+
+//
+// Test Group
+//
+struct test_capigeoslinetocurve_data : public capitest::utility {
+    test_capigeoslinetocurve_data() {
+        params_ = GEOSLineToCurveParams_create();
+    }
+
+    ~test_capigeoslinetocurve_data() override {
+        GEOSLineToCurveParams_destroy(params_);
+    }
+
+    GEOSLineToCurveParams* params_;
+};
+
+using group = test_group<test_capigeoslinetocurve_data>;
+using object = group::object;
+
+group test_capigeoslinetocurve_group("capi::GEOSLineToCurve");
+
+//
+// Test Cases
+//
+
+template<>
+template<>
+void object::test<1>()
+{
+    set_test_name("empty input preserved");
+
+    input_ = fromWKT("LINESTRING EMPTY");
+    result_ = GEOSLineToCurve(input_, params_);
+    ensure_equals(GEOSGeomTypeId(result_), GEOS_LINESTRING);
+    ensure(GEOSisEmpty(result_));
+}
+
+template<>
+template<>
+void object::test<2>()
+{
+    set_test_name("curved input copied");
+
+    input_ = fromWKT("CIRCULARSTRING (0 0, 1 1, 2 0)");
+    result_ = GEOSLineToCurve(input_, params_);
+
+    ensure_geometry_equals_identical(result_, input_);
+}
+
+
+template<>
+template<>
+void object::test<3>()
+{
+    set_test_name("LineString to CircularString, non-default conversion parameters");
+
+    input_ = fromWKT("LINESTRING(0 0,13.3975 50,50 86.6025,100 100,150 86.6025,186.6025 50,200 0)");
+
+    ensure(GEOSLineToCurveParams_setRadiusTolerance(params_, 1e-5));
+
+    result_ = GEOSLineToCurve(input_, params_);
+    expected_ = fromWKT("CIRCULARSTRING (0 0, 100 100, 200 0)");
+
+    ensure_geometry_equals_exact(result_, expected_, 1e-3);
+
+    // max step angle too small to allow conversion
+    GEOSGeom_destroy(result_);
+    ensure(GEOSLineToCurveParams_setMaxStepDegrees(params_, 10));
+    result_ = GEOSLineToCurve(input_, params_);
+    ensure_geometry_equals_identical(result_, input_);
+
+    // max angle difference too small to allow conversion
+    GEOSGeom_destroy(result_);
+    ensure(GEOSLineToCurveParams_setMaxStepDegrees(params_, 90));
+    ensure(GEOSLineToCurveParams_setMaxAngleDifferenceDegrees(params_, 1e-14));
+    result_ = GEOSLineToCurve(input_, params_);
+    ensure_geometry_equals_identical(result_, input_);
+}
+
+} // namespace tut

commit 8c7726f8a3c336e0b7db7b000aa8450266222f53
Author: Daniel Baston <dbaston at gmail.com>
Date:   Mon Mar 9 18:16:22 2026 -0400

    CurveBuilder: handle arcs forming a complete circle

diff --git a/src/algorithm/CircularArcs.cpp b/src/algorithm/CircularArcs.cpp
index 07f2bd630..03fc02cda 100644
--- a/src/algorithm/CircularArcs.cpp
+++ b/src/algorithm/CircularArcs.cpp
@@ -62,6 +62,11 @@ CircularArcs::getMidpointAngle(double theta0, double theta2, bool isCCW)
         return getMidpointAngle(theta2, theta0, true);
     }
 
+    if (theta0 == theta2) {
+        // full circle
+        return theta0 + MATH_PI;
+    }
+
     double mid = (theta0 + theta2) / 2;
     if (!Angle::isWithinCCW(mid, theta0, theta2)) {
         mid += MATH_PI;
diff --git a/src/algorithm/CurveBuilder.cpp b/src/algorithm/CurveBuilder.cpp
index c95721e3a..ef3215fc1 100644
--- a/src/algorithm/CurveBuilder.cpp
+++ b/src/algorithm/CurveBuilder.cpp
@@ -161,6 +161,63 @@ CurveBuilder::finishLine() {
     }
 }
 
+/// Interpolates Z and M values from the midpoint of the arc, by:
+/// 1) Determining the two vertices of the linearized arc that bound the original arc midpoint
+/// 2) Assuming that Z and M increase/decrease at a constant rate from the origin of the original
+///    arc to its midpoint, and increase/decrease at a possibly different constant rate from the
+///    midpoint of the original arc to its endpoint.
+/// 3) Computing values of Z and M based on (2).
+static void
+interpolateMidpointZM(CoordinateXYZM& p1, const CircularArc& arc, size_t stop, const CoordinateXY& center)
+{
+    const auto start = arc.getCoordinatePosition();
+    const auto nPoints = stop - start + 1;
+    const CoordinateSequence& points = *arc.getCoordinateSequence();
+
+    // We have an even number of vertices. Calculate Z/M of the control
+    // point in the original arc.
+    CoordinateXYZM a, b;
+    points.getAt(start + nPoints / 2 - 1, a);
+    points.getAt(start + nPoints / 2, b);
+
+    const double thetaA = CircularArcs::getAngle(a, center);
+    const double theta0 = arc.theta0();
+    const double theta1 = CircularArcs::getAngle(p1, center);
+
+    // Assumption: point a has the same angle fraction over [p0, p1] that b has over [p2, p1]
+    const double f = arc.isCCW() ? Angle::fractionCCW(thetaA, theta0, theta1) : 1 - Angle::fractionCCW(thetaA, theta1, theta0);
+
+    const double z0 = points.getZ(start);
+    const double z2 = points.getZ(stop);
+
+    const double m0 = points.getM(start);
+    const double m2 = points.getM(stop);
+
+    p1.z = (a.z + b.z - z0*(1 - f) - z2*(1 - f)) / (2 * f);
+    p1.m = (a.m + b.m - m0*(1 - f) - m2*(1 - f)) / (2 * f);
+}
+
+/// Assigns Z and M values to the midpoint of an arc given points representing a linearized version of the arc.
+/// If the linearized version of the arc has an odd number of points, the Z and M values are taken directly from
+/// the central vertex of the linearized arc. If the linearized version of the arc has an even number of points,
+/// the Z and M values are calculated using the two vertices that bound the midpoint of the arc.
+static void
+getOrInterpolateMidPointZM(CoordinateXYZM& p1, const CircularArc& arc, size_t stop, const CoordinateXY& center) {
+    const auto start = arc.getCoordinatePosition();
+    const auto nPoints = stop - start + 1;
+    const CoordinateSequence& points = *arc.getCoordinateSequence();
+
+    if (nPoints % 2) {
+        // We have an odd number of vertices, so the central vertex should be the same
+        // as the control point in the original arc.
+        auto midpointIndex = start + nPoints / 2;
+        p1.z = points.getZ(midpointIndex);
+        p1.m = points.getM(midpointIndex);
+    } else {
+        interpolateMidpointZM(p1, arc, stop, center);
+    }
+}
+
 void
 CurveBuilder::addArc(const CircularArc& arc, std::size_t stop) {
     finishLine();
@@ -183,50 +240,71 @@ CurveBuilder::addArc(const CircularArc& arc, std::size_t stop) {
 
     const CoordinateXY averageCenter = { xSum / static_cast<double>(nArcApproximations), ySum / static_cast<double>(nArcApproximations) };
 
-    CoordinateXYZM p0, p1, p2;
+    CoordinateXYZM p0, p2;
     points.getAt(start, p0);
     points.getAt(stop, p2);
 
     const double averageRadius = 0.5*(p0.distance(averageCenter) + p2.distance(averageCenter));
 
-    p1 = CircularArcs::getMidpoint(p0, p2, averageCenter, averageRadius, arc.isCCW());
+    if (p0.equals2D(p2)) {
+        // Arc forms a complete circle
+        const bool isCCW = CircularArc(points, start).isCCW();
 
-    if (points.hasZ() || points.hasM()) {
-        auto nPoints = stop - start + 1;
+        CoordinateXYZM p1(CircularArcs::getMidpoint(p0, p0, averageCenter, averageRadius, isCCW));
 
-        if (nPoints % 2) {
-            // We have an odd number of vertices, so the central vertex should be the same
-            // as the control point in the original arc.
-            auto midpointIndex = start + nPoints / 2;
-            p1.z = points.getZ(midpointIndex);
-            p1.m = points.getM(midpointIndex);
-        } else {
-            // We have an even number of vertices. Calculate Z/M of the control
-            // point in the original arc.
-            CoordinateXYZM a, b;
-            points.getAt(start + nPoints / 2 - 1, a);
-            points.getAt(start + nPoints / 2, b);
+        CoordinateXYZM p01(CircularArcs::getMidpoint(p0, p1, averageCenter, averageRadius, isCCW));
+        CoordinateXYZM p12(CircularArcs::getMidpoint(p1, p2, averageCenter, averageRadius, isCCW));
 
-            const double thetaA = CircularArcs::getAngle(a, averageCenter);
-            const double theta0 = CircularArcs::getAngle(p0, averageCenter);
-            const double theta1 = CircularArcs::getAngle(p1, averageCenter);
-
-            // Assumption: point a has the same angle fraction over [p0, p1] that b has over [p2, p1]
-            const double f = arc.isCCW() ? Angle::fractionCCW(thetaA, theta0, theta1) : 1 - Angle::fractionCCW(thetaA, theta1, theta0);
-
-            p1.z = (a.z + b.z - p0.z*(1 - f) - p2.z*(1 - f)) / (2 * f);
-            p1.m = (a.m + b.m - p0.m*(1 - f) - p2.m*(1 - f)) / (2 * f);
+        if (!arcCoords) {
+            // should always be the case
+            arcCoords = std::make_shared<CoordinateSequence>(0,  points.hasZ(), points.hasM());
+            arcCoords->reserve(5);
+            arcCoords->add(p0);
         }
-    }
 
-    if (!arcCoords) {
-        arcCoords = std::make_shared<CoordinateSequence>(0,  points.hasZ(), points.hasM());
-        arcCoords->reserve(3);
-        arcCoords->add(p0);
-    }
+        if (points.hasZ() || points.hasM()) {
+            auto nPoints = stop - start + 1;
+            if ((nPoints - 1) % 4 == 0) {
+                auto i01 = start + (nPoints - 1) / 4;
+                p01.z = points.getZ(i01);
+                p01.m = points.getM(i01);
 
-    arcCoords->add(p1);
-    arcCoords->add(p2);
+                auto i1 = start + (nPoints - 1) / 2;
+                p1.z = points.getZ(i1);
+                p1.m = points.getM(i1);
+
+                auto i12 = start + 3 * (nPoints - 1) / 4;
+                p12.z = points.getZ(i12);
+                p12.m = points.getM(i12);
+            } else {
+                getOrInterpolateMidPointZM(p1, arc, stop, averageCenter);
+                p01.z = 0.5*(p0.z + p1.z);
+                p01.m = 0.5*(p0.m + p1.m);
+                p12.z = 0.5*(p1.z + p2.z);
+                p12.m = 0.5*(p1.m + p2.m);
+            }
+        }
+
+        arcCoords->add(p01);
+        arcCoords->add(p1);
+        arcCoords->add(p12);
+        arcCoords->add(p2);
+    } else {
+        CoordinateXYZM p1(CircularArcs::getMidpoint(p0, p2, averageCenter, averageRadius, arc.isCCW()));
+
+        if (points.hasZ() || points.hasM()) {
+            getOrInterpolateMidPointZM(p1, arc, stop, averageCenter);
+        }
+
+        if (!arcCoords) {
+            arcCoords = std::make_shared<CoordinateSequence>(0,  points.hasZ(), points.hasM());
+            arcCoords->reserve(3);
+            arcCoords->add(p0);
+        }
+
+        arcCoords->add(p1);
+        arcCoords->add(p2);
+    }
 }
 
 }
diff --git a/tests/unit/algorithm/CircularArcsTest.cpp b/tests/unit/algorithm/CircularArcsTest.cpp
index cb1da46c8..07eda9cab 100644
--- a/tests/unit/algorithm/CircularArcsTest.cpp
+++ b/tests/unit/algorithm/CircularArcsTest.cpp
@@ -955,4 +955,19 @@ void object::test<69>()
     ensure_equals(b.distance(a), 0);
 }
 
+template<>
+template<>
+void object::test<70>()
+{
+set_test_name("getMidpointAngle");
+
+    // half-circle
+    ensure_equals(CircularArcs::getMidpointAngle(0, MATH_PI, true), MATH_PI/2);
+    ensure_equals(CircularArcs::getMidpointAngle(0, MATH_PI, false), 3*MATH_PI/2);
+
+    // full circle
+    ensure_equals(CircularArcs::getMidpointAngle(0, 0, true), MATH_PI);
+    ensure_equals(CircularArcs::getMidpointAngle(0, 0, false), MATH_PI);
+}
+
 }
diff --git a/tests/unit/algorithm/CurveBuilderTest.cpp b/tests/unit/algorithm/CurveBuilderTest.cpp
index 3d54db39b..2e45df6d4 100644
--- a/tests/unit/algorithm/CurveBuilderTest.cpp
+++ b/tests/unit/algorithm/CurveBuilderTest.cpp
@@ -7,6 +7,7 @@
 #include "utility.h"
 
 using geos::algorithm::CurveToLineParams;
+using geos::algorithm::LineToCurveParams;
 
 namespace tut {
 
@@ -275,5 +276,35 @@ void object::test<17>() {
     checkRoundTripUnchanged("CIRCULARSTRING (426858 5427938,500000 5538632,573142 5427938)", 4);
 }
 
+template<>
+template<>
+void object::test<18>()
+{
+    set_test_name("full circle, 2D");
+
+    auto geom = reader_.read("POINT (0 0)")->buffer(5);
+    LineToCurveParams params;
+    auto curved = geom->getCurved(params);
+
+    auto expected = reader_.read("CURVEPOLYGON (CIRCULARSTRING (5 0, 0 -5, -5 0, 0 5, 5 0))");
+
+    ensure_equals_exact_geometry_xyzm(curved.get(), expected.get(), 1e-8);
+}
+
+template<>
+template<>
+void object::test<19>()
+{
+    set_test_name("full circle, XYZM");
+
+    // with step size of 11.25 degrees, control points of the arc will appear in linearized
+    // arc, so their Z/M values can be recovered for the result arcs
+    checkRoundTripUnchanged("CIRCULARSTRING ZM (-5 0 4 3, 0 5 8 6, 5 0 12 9, 0 -5 15 11, -5 0 18 13)", 11.25);
+
+    // with step size of 4 degrees, Z/M values for the result arc control points must be
+    // interpolated from points in the linearized arc.
+    checkRoundTripUnchanged("CIRCULARSTRING ZM (-5 0 4 3, 0 5 8 6, 5 0 12 9, 0 -5 15 11, -5 0 18 13)", 4);
+}
+
 
 }

commit 7307e49f43d11c456e14d3b0c814316bd5417de3
Author: Daniel Baston <dbaston at gmail.com>
Date:   Thu Feb 12 12:58:05 2026 -0500

    CurveBuilder: expose tolerance for max angle step

diff --git a/include/geos/algorithm/CurveBuilder.h b/include/geos/algorithm/CurveBuilder.h
index d99de3771..1739dc540 100644
--- a/include/geos/algorithm/CurveBuilder.h
+++ b/include/geos/algorithm/CurveBuilder.h
@@ -58,9 +58,6 @@ private:
     std::vector<std::unique_ptr<geom::SimpleCurve>> curves;
     const geom::GeometryFactory& factory;
 
-    /// Maximum angle between two vertices to be considered part of the same arc.
-    static constexpr double maxSpacingRadians = MATH_PI / 4 + 0.01;
-
     /// Declared as non-copyable
     CurveBuilder(const CurveBuilder& other);
     CurveBuilder& operator=(const CurveBuilder& rhs);
diff --git a/include/geos/algorithm/CurveToLineParams.h b/include/geos/algorithm/CurveToLineParams.h
index a4daa9491..9e1ba5deb 100644
--- a/include/geos/algorithm/CurveToLineParams.h
+++ b/include/geos/algorithm/CurveToLineParams.h
@@ -27,9 +27,28 @@ public:
         MAX_DEVIATION,
     };
 
-    CurveToLineParams(TOLERANCE_TYPE tolType, double tolValue) :
-        toleranceType(tolType),
-        toleranceValue(tolValue) {}
+    CurveToLineParams() : CurveToLineParams(TOLERANCE_TYPE::STEP_DEGREES, 4.0) {}
+
+    CurveToLineParams(TOLERANCE_TYPE tolType, double tolValue) {
+        setTolerance(tolType, tolValue);
+    }
+
+    void setTolerance(TOLERANCE_TYPE tolType, double tolValue) {
+        if (tolType == TOLERANCE_TYPE::STEP_DEGREES) {
+            if (!(tolValue > 0)) {
+                throw util::IllegalArgumentException("Step size must be positive");
+            }
+        } else if (tolType == TOLERANCE_TYPE::MAX_DEVIATION) {
+            if (!(tolValue > 0)) {
+                throw util::IllegalArgumentException("Max deviation must be positive");
+            }
+        } else {
+            throw util::IllegalArgumentException("Invalid tolerance type");
+        }
+
+        toleranceType = tolType;
+        toleranceValue = tolValue;
+    }
 
     static CurveToLineParams maxDeviation(double dev) {
         return CurveToLineParams(TOLERANCE_TYPE::MAX_DEVIATION, dev);
@@ -48,12 +67,13 @@ public:
             return std::acos(1 - toleranceValue / arc.getRadius()) * 360 / MATH_PI;
         }
 
-        throw util::GEOSException("Invalid tolerance type");
+        throw util::IllegalArgumentException("Invalid tolerance type");
     }
 
 private:
-    const TOLERANCE_TYPE toleranceType;
-    const double toleranceValue;
+
+    TOLERANCE_TYPE toleranceType;
+    double toleranceValue;
 
 };
 
diff --git a/include/geos/algorithm/LineToCurveParams.h b/include/geos/algorithm/LineToCurveParams.h
index 572f14b16..9690df289 100644
--- a/include/geos/algorithm/LineToCurveParams.h
+++ b/include/geos/algorithm/LineToCurveParams.h
@@ -22,34 +22,54 @@ class GEOS_DLL LineToCurveParams {
 
 public:
 
-    static LineToCurveParams getDefault() {
-        return {};
-    }
-
-    // Get the radius tolerance. For p[x] to be considered a continuation of the same arc, it must
-    // be within the tolerance of the arc radius as computed from p[0], p[1], p[2].
+    // Get the radius tolerance. For p[x] to be considered a continuation of the same arc,
+    // its radius must be within tol * the arc radius as computed from p[0], p[1], p[2].
     double getRadiusTolerance() const {
         return radiusTolerance;
     }
 
     void setRadiusTolerance(double tol) {
+        if (!(tol > 0)) {
+            throw util::IllegalArgumentException("Radius tolerance must be positive");
+        }
         radiusTolerance = tol;
     }
 
-    // Get the angle step tolerance. For p[x] to be considered a continuation of the same arc, the angle
-    // formed by p[x-1] - center - p[x] must be within this fraction of the angle formed by
-    // p[x-2] - center - p[x-1].
-    double getAngleStepTolerance() const {
-        return angleStepTolerance;
+    double getMaxExteriorAngleDifferenceRadians() const {
+        return maxExteriorAngleDifferenceRadians;
     }
 
-    void setAngleStepTolerance(double tol) {
-        angleStepTolerance = tol;
+    void setMaxExteriorAngleDifferenceRadians(double tol) {
+        if (!(tol > 0)) {
+            throw util::IllegalArgumentException("Angle tolerance must be positive");
+        }
+        maxExteriorAngleDifferenceRadians = tol;
+    }
+
+    void setMaxExteriorAngleDifferenceDegrees(double tol) {
+        setMaxExteriorAngleDifferenceRadians(tol * MATH_PI / 180.0);
+    }
+
+    // Get the maximum angle step.
+    double getMaxAngleDegrees() const {
+        return maxAngleRadians * 180 / MATH_PI;
+    }
+
+    double getMaxAngleRadians() const {
+        return maxAngleRadians;
+    }
+
+    void setMaxStepDegrees(double tol) {
+        if (!(tol > 0)) {
+            throw util::IllegalArgumentException("Angle step tolerance must be positive");
+        }
+        maxAngleRadians = tol * MATH_PI / 180.0;
     }
 
 private:
     double radiusTolerance{1e-6};
-    double angleStepTolerance{1e-3};
+    double maxAngleRadians{45.01 * MATH_PI / 180.0};
+    double maxExteriorAngleDifferenceRadians{0.01};
 };
 
 }
\ No newline at end of file
diff --git a/src/algorithm/CurveBuilder.cpp b/src/algorithm/CurveBuilder.cpp
index e5aa22877..c95721e3a 100644
--- a/src/algorithm/CurveBuilder.cpp
+++ b/src/algorithm/CurveBuilder.cpp
@@ -43,6 +43,10 @@ CurveBuilder::getCurved(const LineString &ls, const LineToCurveParams& params) {
 
 std::unique_ptr<geom::Curve>
 CurveBuilder::compute(const LineString& ls, const LineToCurveParams& params) {
+    if (ls.isEmpty()) {
+        return ls.clone();
+    }
+
     const auto& points = *ls.getSharedCoordinates();
 
     std::size_t start = 0;
@@ -56,12 +60,18 @@ CurveBuilder::compute(const LineString& ls, const LineToCurveParams& params) {
 
         CircularArc arc(points, start);
 
-        if (arc.isLinear() || arc.getAngle() > 2*maxSpacingRadians) {
+        if (arc.isLinear()) {
             addLineCoords(points, start, start + 2);
             start += 2;
             continue;
         }
 
+        if (arc.getAngle() > 2 *params.getMaxAngleRadians()) {
+            addLineCoords(points, start, start + 1);
+            start++;
+            continue;
+        }
+
         std::size_t stop = start + 3;
         bool foundArc = false;
 
@@ -73,18 +83,27 @@ CurveBuilder::compute(const LineString& ls, const LineToCurveParams& params) {
             const double distance = pt.distance(arc.getCenter());
 
             // Does the radius match?
-            if (std::abs(distance - arc.getRadius()) > params.getRadiusTolerance()) {
+            if (std::abs(distance - arc.getRadius()) > params.getRadiusTolerance() * arc.getRadius()) {
                 break;
             }
 
             const CoordinateXY& prev1 = points.getAt<CoordinateXY>(stop - 1);
             const CoordinateXY& prev2 = points.getAt<CoordinateXY>(stop - 2);
 
-            // Is the angle similar to, or less than, the previous ones?
-            const double prevAngle = Angle::angleBetween(arc.p0(), arc.p1(), arc.p2());
-            const double currAngle = Angle::angleBetween(prev2, prev1, pt);
+            // Check that angle p[stop-1] /_ center /_ p[stop] is less than the step tolerance.
+            // This check is not done in PostGIS.
+            const double currAngle = std::abs(Angle::angleBetweenOriented(prev1, arc.getCenter(), pt));
 
-            if (std::abs(currAngle - prevAngle) > prevAngle * params.getAngleStepTolerance()) {
+            if (currAngle > params.getMaxAngleRadians()) {
+                break;
+            }
+
+            // Check that the angle p[stop-2] /_ p[stop-1] /_ p[stop] is consistent with that in the
+            // original section of the arc.
+            const double prevExtAngle = Angle::angleBetween(arc.p0(), arc.p1(), arc.p2());
+            const double currExtAngle = Angle::angleBetween(prev2, prev1, pt);
+
+            if (std::abs(currExtAngle - prevExtAngle) > params.getMaxExteriorAngleDifferenceRadians()) {
                 break;
             }
 
diff --git a/tests/unit/algorithm/CurveBuilderTest.cpp b/tests/unit/algorithm/CurveBuilderTest.cpp
index 08192c373..3d54db39b 100644
--- a/tests/unit/algorithm/CurveBuilderTest.cpp
+++ b/tests/unit/algorithm/CurveBuilderTest.cpp
@@ -21,10 +21,13 @@ struct test_algorithm_curvebuilder_data {
 
         auto ltcParams = geos::algorithm::LineToCurveParams();
         ltcParams.setRadiusTolerance(1e-6);
+        ltcParams.setMaxStepDegrees(stepSizeDegrees*1.001);
 
         auto outCurve = linearized->getCurved(ltcParams);
         auto expected = reader_.read(wkt_expected);
 
+        std::cout << outCurve->toString() << std::endl;
+
         double distanceTolerance = 1e-4;
 
         ensure_equals_exact_geometry_xyzm(outCurve.get(), expected.get(), distanceTolerance);
@@ -34,11 +37,12 @@ struct test_algorithm_curvebuilder_data {
         checkRoundTrip(wkt_in, wkt_in, stepSizeDegrees);
     }
 
-    void checkLineToCurve(const std::string& wkt_in, const std::string& wkt_expected, double distanceTolerance) const {
+    void checkLineToCurve(const std::string& wkt_in, const std::string& wkt_expected, double distanceTolerance, double stepSizeDegrees) const {
         auto ls = reader_.read<LineString>(wkt_in);
 
         auto ltcParams = geos::algorithm::LineToCurveParams();
         ltcParams.setRadiusTolerance(distanceTolerance);
+        ltcParams.setMaxStepDegrees(stepSizeDegrees);
 
         auto curve = ls->getCurved(ltcParams);
         auto expected = reader_.read(wkt_expected);
@@ -89,7 +93,7 @@ void object::test<3>()
 
     checkLineToCurve("LINESTRING(0 0,29.2893 70.7107,100 100,170.7107 70.7107,200 0)",
                      "CIRCULARSTRING (0 0, 100 100, 200 0)",
-                     2e-3);
+                     2e-3, 45.1);
 }
 
 template<>
@@ -100,7 +104,7 @@ void object::test<4>()
 
     checkLineToCurve("LINESTRING(0 0,29.2893 70.7107,100 100,170.7107 70.7107,200 0, 0 0)",
                      "COMPOUNDCURVE (CIRCULARSTRING (0 0, 100 100, 200 0), LINESTRING (200 0, 0 0))",
-                     2.5e-3);
+                     2.5e-3, 45.1);
 }
 
 template<>
diff --git a/tests/unit/geom/CircularStringTest.cpp b/tests/unit/geom/CircularStringTest.cpp
index 32e693e91..f8d0ad5c4 100644
--- a/tests/unit/geom/CircularStringTest.cpp
+++ b/tests/unit/geom/CircularStringTest.cpp
@@ -507,7 +507,7 @@ void object::test<26>()
     auto cs = wktreader_.read<CircularString>("CIRCULARSTRING(0 0, 1 1, 2 0)");
 
     // Check that we return Curve* rather than Geometry*
-    std::unique_ptr<geos::geom::Curve> curved = cs->getCurved(LineToCurveParams::getDefault());
+    std::unique_ptr<geos::geom::Curve> curved = cs->getCurved(LineToCurveParams());
 
     ensure_equals_exact_geometry_xyzm(curved.get(), cs.get(), 0);
 }
diff --git a/tests/unit/geom/CompoundCurveTest.cpp b/tests/unit/geom/CompoundCurveTest.cpp
index e74610288..a156a45d0 100644
--- a/tests/unit/geom/CompoundCurveTest.cpp
+++ b/tests/unit/geom/CompoundCurveTest.cpp
@@ -450,7 +450,7 @@ void object::test<13>()
     set_test_name("getCurved()");
 
     // check that we return Curve* rather than Geometry*
-    std::unique_ptr<Curve> curved = cc_->getCurved(geos::algorithm::LineToCurveParams::getDefault());
+    std::unique_ptr<Curve> curved = cc_->getCurved(geos::algorithm::LineToCurveParams());
 
     ensure_equals_exact_geometry_xyzm(curved.get(), cc_.get(), 0);
 }
diff --git a/tests/unit/geom/CurvePolygonTest.cpp b/tests/unit/geom/CurvePolygonTest.cpp
index 41e603cce..52eb905b2 100644
--- a/tests/unit/geom/CurvePolygonTest.cpp
+++ b/tests/unit/geom/CurvePolygonTest.cpp
@@ -302,7 +302,7 @@ void object::test<8>()
 {
     set_test_name("getCurved");
 
-    std::unique_ptr<CurvePolygon> curved = cp_->getCurved(geos::algorithm::LineToCurveParams::getDefault());
+    std::unique_ptr<CurvePolygon> curved = cp_->getCurved(geos::algorithm::LineToCurveParams());
 
     ensure_equals_exact_geometry_xyzm(curved.get(), cp_.get(), 0);
 }
diff --git a/tests/unit/geom/GeometryCollectionTest.cpp b/tests/unit/geom/GeometryCollectionTest.cpp
index 5a847b913..95df45ab4 100644
--- a/tests/unit/geom/GeometryCollectionTest.cpp
+++ b/tests/unit/geom/GeometryCollectionTest.cpp
@@ -203,11 +203,11 @@ void object::test<10>() {
 template<>
 template<>
 void object::test<11>() {
-    set_test_name("getLinearized()");
+    set_test_name("getCurved()");
 
     auto gc = readWKT("GEOMETRYCOLLECTION(POINT (3 7), LINESTRING (0 0, 0.292893 0.707107, 1 1, 1.707107 0.707107, 2 0), LINESTRING (1 1, 2 3))");
 
-    auto params = LineToCurveParams::getDefault();
+    auto params = LineToCurveParams();
     params.setRadiusTolerance(1e-3);
 
     auto curved = gc->getCurved(params);
diff --git a/tests/unit/geom/LineStringTest.cpp b/tests/unit/geom/LineStringTest.cpp
index 20b2e7454..bae07c13a 100644
--- a/tests/unit/geom/LineStringTest.cpp
+++ b/tests/unit/geom/LineStringTest.cpp
@@ -668,7 +668,7 @@ void object::test<37>()
     auto input = reader.read<LineString>("LINESTRING (2 2, 2.292893 2.707107, 3 3, 3.707107 2.707107, 4 2, 2 2)");
 
     // check that we return Curve* instead of Geometry*
-    auto params = geos::algorithm::LineToCurveParams::getDefault();
+    auto params = geos::algorithm::LineToCurveParams();
     params.setRadiusTolerance(1e-3);
 
     std::unique_ptr<geos::geom::Curve> curved = input->getCurved(params);
diff --git a/tests/unit/geom/MultiCurveTest.cpp b/tests/unit/geom/MultiCurveTest.cpp
index d85699f74..e4f264c8c 100644
--- a/tests/unit/geom/MultiCurveTest.cpp
+++ b/tests/unit/geom/MultiCurveTest.cpp
@@ -224,7 +224,7 @@ void object::test<6>()
 {
     set_test_name("getCurved()");
 
-    std::unique_ptr<geos::geom::GeometryCollection> curved = mc_->getCurved(geos::algorithm::LineToCurveParams::getDefault());
+    std::unique_ptr<geos::geom::GeometryCollection> curved = mc_->getCurved(geos::algorithm::LineToCurveParams());
     ensure_equals_exact_geometry_xyzm(mc_.get(), curved.get(), 0);
 }
 
diff --git a/tests/unit/geom/MultiLineStringTest.cpp b/tests/unit/geom/MultiLineStringTest.cpp
index 258f80b39..1851a2683 100644
--- a/tests/unit/geom/MultiLineStringTest.cpp
+++ b/tests/unit/geom/MultiLineStringTest.cpp
@@ -101,7 +101,7 @@ void object::test<6>()
 
     auto input = reader_.read<MultiLineString>("MULTILINESTRING ((3 3, 4 4), (-2 0, -1.414 1.414, 0 2, 1.414 1.414, 2 0, 2 3))");
 
-    auto params = geos::algorithm::LineToCurveParams::getDefault();
+    auto params = geos::algorithm::LineToCurveParams();
 
     // tolerance is too fine to generate a MultiCurve
     params.setRadiusTolerance(1e-9);
diff --git a/tests/unit/geom/MultiPolygonTest.cpp b/tests/unit/geom/MultiPolygonTest.cpp
index 9789ba6dc..465828688 100644
--- a/tests/unit/geom/MultiPolygonTest.cpp
+++ b/tests/unit/geom/MultiPolygonTest.cpp
@@ -96,7 +96,7 @@ void object::test<6>()
         "MULTIPOLYGON (((0 0, 10 0, 10 10, 0 10, 0 0), (2 2, 2.292893 2.707107, 3 3, 3.707107 2.707107, 4 2, 2 2)),"
                                      "((20 0, 30 0, 30 10, 20 0)))");
 
-    auto params = geos::algorithm::LineToCurveParams::getDefault();
+    auto params = geos::algorithm::LineToCurveParams();
 
     // Tolerance to small to allow conversion to MultiSurface
     params.setRadiusTolerance(1e-12);
diff --git a/tests/unit/geom/MultiSurfaceTest.cpp b/tests/unit/geom/MultiSurfaceTest.cpp
index 0e32439a1..9f84cdc28 100644
--- a/tests/unit/geom/MultiSurfaceTest.cpp
+++ b/tests/unit/geom/MultiSurfaceTest.cpp
@@ -194,7 +194,7 @@ void object::test<5>()
 {
     set_test_name("getCurved()");
 
-    std::unique_ptr<geos::geom::GeometryCollection> curved = ms_->getCurved(geos::algorithm::LineToCurveParams::getDefault());
+    std::unique_ptr<geos::geom::GeometryCollection> curved = ms_->getCurved(geos::algorithm::LineToCurveParams());
 
     ensure_equals_exact_geometry_xyzm(ms_.get(), curved.get(), 0);
 }
diff --git a/tests/unit/geom/PointTest.cpp b/tests/unit/geom/PointTest.cpp
index 26bcf2858..dbfa5c8af 100644
--- a/tests/unit/geom/PointTest.cpp
+++ b/tests/unit/geom/PointTest.cpp
@@ -649,7 +649,7 @@ void object::test<50>()
 {
     set_test_name("getCurved");
 
-    ensure_equals_exact_geometry_xyzm(point_.get(), point_->getCurved(geos::algorithm::LineToCurveParams::getDefault()).get(), 0);
+    ensure_equals_exact_geometry_xyzm(point_.get(), point_->getCurved(geos::algorithm::LineToCurveParams()).get(), 0);
 }
 
 } // namespace tut
diff --git a/tests/unit/geom/PolygonTest.cpp b/tests/unit/geom/PolygonTest.cpp
index eee67405e..dc35e52f4 100644
--- a/tests/unit/geom/PolygonTest.cpp
+++ b/tests/unit/geom/PolygonTest.cpp
@@ -772,7 +772,7 @@ void object::test<50>()
     auto input = reader.read<Polygon>("POLYGON ((0 0, 0.2675 -0.3446, 0.6464 -0.5607, 1.0793 -0.6152, 1.5 -0.5, 1.8446 -0.2325, 2.0607 0.1464, 2.1152 0.5793, 2 1, 1.6934 1.4588, 1.5858 2, 1.6934 2.5412, 2 3, 2.4588 3.3066, 3 3.4142, 3.5412 3.3066, 4 3, 4 5, 1 4, 0 0), (1.7 1, 1.5871 1.0537, 1.4623 1.0629, 1.3427 1.0265, 1.2444 0.9492, 1.1806 0.8416, 1.16 0.7183, 1.1855 0.5958, 1.2534 0.4908, 1.3548 0.4175, 1.4757 0.3858, 1.6 0.4, 1.6203 0.705, 1.7 1))");
 
     // check that we return Surface* rather than Geometry*
-    auto params = geos::algorithm::LineToCurveParams::getDefault();
+    auto params = geos::algorithm::LineToCurveParams();
     params.setRadiusTolerance(0.01);
 
     std::unique_ptr<geos::geom::Surface> curved = input->getCurved(params);

commit 28712d504783adf4da7b34b125f245e0d1a99915
Author: Daniel Baston <dbaston at gmail.com>
Date:   Tue Feb 10 13:28:30 2026 -0500

    Curve-Line conversion: use conversion parameter classes

diff --git a/include/geos/algorithm/CurveBuilder.h b/include/geos/algorithm/CurveBuilder.h
index 5b869a13e..d99de3771 100644
--- a/include/geos/algorithm/CurveBuilder.h
+++ b/include/geos/algorithm/CurveBuilder.h
@@ -18,6 +18,10 @@
 
 #include <memory>
 
+namespace geos::algorithm {
+    class LineToCurveParams;
+}
+
 namespace geos::geom {
     class CircularArc;
     class CoordinateSequence;
@@ -33,13 +37,13 @@ class GEOS_DLL CurveBuilder {
 
 public:
 
-    static std::unique_ptr<geom::Curve> getCurved(const geom::LineString& ls, double distanceTolerance);
+    static std::unique_ptr<geom::Curve> getCurved(const geom::LineString& ls, const LineToCurveParams& params);
 
 private:
 
     explicit CurveBuilder(const geom::GeometryFactory& factory);
 
-    std::unique_ptr<geom::Curve> compute(const geom::LineString& ls, double distanceTolerance);
+    std::unique_ptr<geom::Curve> compute(const geom::LineString& ls, const LineToCurveParams& params);
 
     void addArc(const geom::CircularArc& arc, std::size_t stop);
 
diff --git a/include/geos/algorithm/CurveToLineParams.h b/include/geos/algorithm/CurveToLineParams.h
new file mode 100644
index 000000000..a4daa9491
--- /dev/null
+++ b/include/geos/algorithm/CurveToLineParams.h
@@ -0,0 +1,60 @@
+/**********************************************************************
+*
+ * GEOS - Geometry Engine Open Source
+ * http://geos.osgeo.org
+ *
+ * Copyright (C) 2026 ISciences, LLC
+ *
+ * This is free software; you can redistribute and/or modify it under
+ * the terms of the GNU Lesser General Public Licence as published
+ * by the Free Software Foundation.
+ * See the COPYING file for more information.
+ *
+ **********************************************************************/
+
+#pragma once
+
+#include <geos/export.h>
+#include <geos/geom/CircularArc.h>
+
+namespace geos::algorithm {
+
+class GEOS_DLL CurveToLineParams {
+
+public:
+    enum class TOLERANCE_TYPE {
+        STEP_DEGREES,
+        MAX_DEVIATION,
+    };
+
+    CurveToLineParams(TOLERANCE_TYPE tolType, double tolValue) :
+        toleranceType(tolType),
+        toleranceValue(tolValue) {}
+
+    static CurveToLineParams maxDeviation(double dev) {
+        return CurveToLineParams(TOLERANCE_TYPE::MAX_DEVIATION, dev);
+    }
+
+    static CurveToLineParams stepSizeDegrees(double stepSize) {
+        return CurveToLineParams(TOLERANCE_TYPE::STEP_DEGREES, stepSize);
+    }
+
+    double getStepSizeDegrees(const geom::CircularArc& arc) const {
+        if (toleranceType == TOLERANCE_TYPE::STEP_DEGREES) {
+            return toleranceValue;
+        }
+
+        if (toleranceType == TOLERANCE_TYPE::MAX_DEVIATION) {
+            return std::acos(1 - toleranceValue / arc.getRadius()) * 360 / MATH_PI;
+        }
+
+        throw util::GEOSException("Invalid tolerance type");
+    }
+
+private:
+    const TOLERANCE_TYPE toleranceType;
+    const double toleranceValue;
+
+};
+
+}
\ No newline at end of file
diff --git a/include/geos/algorithm/LineToCurveParams.h b/include/geos/algorithm/LineToCurveParams.h
new file mode 100644
index 000000000..572f14b16
--- /dev/null
+++ b/include/geos/algorithm/LineToCurveParams.h
@@ -0,0 +1,55 @@
+/**********************************************************************
+*
+ * GEOS - Geometry Engine Open Source
+ * http://geos.osgeo.org
+ *
+ * Copyright (C) 2026 ISciences, LLC
+ *
+ * This is free software; you can redistribute and/or modify it under
+ * the terms of the GNU Lesser General Public Licence as published
+ * by the Free Software Foundation.
+ * See the COPYING file for more information.
+ *
+ **********************************************************************/
+
+#pragma once
+
+#include <geos/export.h>
+
+namespace geos::algorithm {
+
+class GEOS_DLL LineToCurveParams {
+
+public:
+
+    static LineToCurveParams getDefault() {
+        return {};
+    }
+
+    // Get the radius tolerance. For p[x] to be considered a continuation of the same arc, it must
+    // be within the tolerance of the arc radius as computed from p[0], p[1], p[2].
+    double getRadiusTolerance() const {
+        return radiusTolerance;
+    }
+
+    void setRadiusTolerance(double tol) {
+        radiusTolerance = tol;
+    }
+
+    // Get the angle step tolerance. For p[x] to be considered a continuation of the same arc, the angle
+    // formed by p[x-1] - center - p[x] must be within this fraction of the angle formed by
+    // p[x-2] - center - p[x-1].
+    double getAngleStepTolerance() const {
+        return angleStepTolerance;
+    }
+
+    void setAngleStepTolerance(double tol) {
+        angleStepTolerance = tol;
+    }
+
+private:
+    double radiusTolerance{1e-6};
+    double angleStepTolerance{1e-3};
+};
+
+}
\ No newline at end of file
diff --git a/include/geos/geom/CircularArc.h b/include/geos/geom/CircularArc.h
index cdb50362a..662075ff3 100644
--- a/include/geos/geom/CircularArc.h
+++ b/include/geos/geom/CircularArc.h
@@ -21,6 +21,11 @@
 #include <geos/algorithm/CircularArcs.h>
 #include <geos/algorithm/Orientation.h>
 
+// Forward declarations
+namespace geos::algorithm {
+class CurveToLineParams;
+}
+
 namespace geos {
 namespace geom {
 
@@ -119,7 +124,7 @@ public:
 
     /// Add linearized points representing this arc to the provided CoordinateSequence.
     /// The origin point of the arc will NOT be added.
-    void addLinearizedPoints(CoordinateSequence& seq, double stepDegrees) const;
+    void addLinearizedPoints(CoordinateSequence& seq, const algorithm::CurveToLineParams& params) const;
 
     /// Return the orientation of the arc as one of:
     /// - algorithm::Orientation::CLOCKWISE,
diff --git a/include/geos/geom/CircularString.h b/include/geos/geom/CircularString.h
index b42af6ad8..e462e8af5 100644
--- a/include/geos/geom/CircularString.h
+++ b/include/geos/geom/CircularString.h
@@ -56,7 +56,7 @@ public:
         return std::unique_ptr<CircularString>(reverseImpl());
     }
 
-    std::unique_ptr<Curve> getCurved(double distanceTolerance) const;
+    std::unique_ptr<Curve> getCurved(const algorithm::LineToCurveParams&) const;
 
 protected:
 
@@ -79,11 +79,11 @@ protected:
         envelope = computeEnvelopeInternal(false);
     }
 
-    CircularString* getCurvedImpl(double) const override {
+    CircularString* getCurvedImpl(const algorithm::LineToCurveParams&) const override {
         return cloneImpl();
     }
 
-    LineString* getLinearizedImpl(double) const override;
+    LineString* getLinearizedImpl(const algorithm::CurveToLineParams&) const override;
 
     int
     getSortIndex() const override
diff --git a/include/geos/geom/CompoundCurve.h b/include/geos/geom/CompoundCurve.h
index 6bedc9ce5..62873fe5c 100644
--- a/include/geos/geom/CompoundCurve.h
+++ b/include/geos/geom/CompoundCurve.h
@@ -118,9 +118,9 @@ protected:
         envelope = computeEnvelopeInternal();
     }
 
-    LineString* getLinearizedImpl(double) const override;
+    LineString* getLinearizedImpl(const algorithm::CurveToLineParams&) const override;
 
-    CompoundCurve* getCurvedImpl(double) const override { return cloneImpl(); }
+    CompoundCurve* getCurvedImpl(const algorithm::LineToCurveParams&) const override { return cloneImpl(); }
 
     int getSortIndex() const override
     {
diff --git a/include/geos/geom/Curve.h b/include/geos/geom/Curve.h
index 8126f829f..38ce5ee58 100644
--- a/include/geos/geom/Curve.h
+++ b/include/geos/geom/Curve.h
@@ -75,9 +75,9 @@ public:
     /// or NULL if this is an EMPTY Curve.
     virtual std::unique_ptr<Point> getStartPoint() const = 0;
 
-    std::unique_ptr<LineString> getLinearized(double degreeSpacing) const;
+    std::unique_ptr<LineString> getLinearized(const algorithm::CurveToLineParams&) const;
 
-    std::unique_ptr<Curve> getCurved(double distanceTolerance) const;
+    std::unique_ptr<Curve> getCurved(const algorithm::LineToCurveParams&) const;
 
     /// Returns true if the first and last coordinate in the Curve are the same
     virtual bool isClosed() const = 0;
@@ -96,7 +96,7 @@ public:
 protected:
     Curve(const GeometryFactory& factory) : Geometry(&factory) {}
 
-    Curve* getCurvedImpl(double distanceTolerance) const override = 0;
+    Curve* getCurvedImpl(const algorithm::LineToCurveParams&) const override = 0;
 };
 
 }
diff --git a/include/geos/geom/CurvePolygon.h b/include/geos/geom/CurvePolygon.h
index 845db9d80..65ba7e66d 100644
--- a/include/geos/geom/CurvePolygon.h
+++ b/include/geos/geom/CurvePolygon.h
@@ -36,11 +36,11 @@ public:
 
     GeometryTypeId getGeometryTypeId() const override;
 
-    std::unique_ptr<CurvePolygon> getCurved(double) {
+    std::unique_ptr<CurvePolygon> getCurved(const algorithm::LineToCurveParams&) {
         return std::unique_ptr<CurvePolygon>(cloneImpl());
     }
 
-    std::unique_ptr<Polygon> getLinearized(double degreeSpacing) const;
+    std::unique_ptr<Polygon> getLinearized(const algorithm::CurveToLineParams&) const;
 
     bool hasCurvedComponents() const override;
 
@@ -51,9 +51,9 @@ protected:
 
     CurvePolygon* cloneImpl() const override;
 
-    Polygon* getLinearizedImpl(double degreeSpacing) const override;
+    Polygon* getLinearizedImpl(const algorithm::CurveToLineParams&) const override;
 
-    CurvePolygon* getCurvedImpl(double) const override { return cloneImpl(); }
+    CurvePolygon* getCurvedImpl(const algorithm::LineToCurveParams&) const override { return cloneImpl(); }
 
     int
     getSortIndex() const override
diff --git a/include/geos/geom/Geometry.h b/include/geos/geom/Geometry.h
index ad4d0d26c..8fb442832 100644
--- a/include/geos/geom/Geometry.h
+++ b/include/geos/geom/Geometry.h
@@ -50,6 +50,10 @@
 
 // Forward declarations
 namespace geos {
+namespace algorithm {
+class CurveToLineParams;
+class LineToCurveParams;
+}
 namespace geom {
 class Coordinate;
 class CoordinateFilter;
@@ -337,22 +341,16 @@ public:
 
     /// Compute an approximation of the Geometry that contains no arcs.
     /// If the Geometry already contains no arcs, a copy will be returned.
-    ///
-    /// \param degreeSpacing The spacing to use between successive points
-    ///        when approximating an arc.
-    std::unique_ptr<Geometry> getLinearized(double degreeSpacing) const
+    std::unique_ptr<Geometry> getLinearized(const algorithm::CurveToLineParams& params) const
     {
-        return std::unique_ptr<Geometry>(getLinearizedImpl(degreeSpacing));
+        return std::unique_ptr<Geometry>(getLinearizedImpl(params));
     }
 
     /// Attempt to replace linear sections of a geometry with arcs, where possible.
     /// Any arcs that already exist in the Geometry will be retained unmodified.
-    ///
-    /// \param distanceTolerance The distance tolerance to use when comparing the center
-    ///        point of arcs computed between different sets of three vertices.
-    std::unique_ptr<Geometry> getCurved(double distanceTolerance) const
+    std::unique_ptr<Geometry> getCurved(const algorithm::LineToCurveParams& params) const
     {
-        return std::unique_ptr<Geometry>(getCurvedImpl(distanceTolerance));
+        return std::unique_ptr<Geometry>(getCurvedImpl(params));
     }
 
     /**
@@ -922,9 +920,9 @@ protected:
     /// Make a geometry with coordinates in reverse order
     virtual Geometry* reverseImpl() const = 0;
 
-    virtual Geometry* getLinearizedImpl(double degreeSpacing) const = 0;
+    virtual Geometry* getLinearizedImpl(const algorithm::CurveToLineParams&) const = 0;
 
-    virtual Geometry* getCurvedImpl(double degreeSpacing) const = 0;
+    virtual Geometry* getCurvedImpl(const algorithm::LineToCurveParams&) const = 0;
 
     /// Returns true if the array contains any non-empty Geometrys.
     template<typename T>
diff --git a/include/geos/geom/GeometryCollection.h b/include/geos/geom/GeometryCollection.h
index a0abe1b7c..1fd3826bb 100644
--- a/include/geos/geom/GeometryCollection.h
+++ b/include/geos/geom/GeometryCollection.h
@@ -168,12 +168,12 @@ public:
     /// Returns the number of geometries in this collection
     std::size_t getNumGeometries() const override;
 
-    std::unique_ptr<GeometryCollection> getCurved(double distanceTolerance) {
-        return std::unique_ptr<GeometryCollection>(getCurvedImpl(distanceTolerance));
+    std::unique_ptr<GeometryCollection> getCurved(const algorithm::LineToCurveParams& params) {
+        return std::unique_ptr<GeometryCollection>(getCurvedImpl(params));
     }
 
-    std::unique_ptr<GeometryCollection> getLinearized(double stepDegrees) {
-        return std::unique_ptr<GeometryCollection>(getLinearizedImpl(stepDegrees));
+    std::unique_ptr<GeometryCollection> getLinearized(const algorithm::CurveToLineParams& params) {
+        return std::unique_ptr<GeometryCollection>(getLinearizedImpl(params));
     }
 
     /// Returns a pointer to the nth Geometry in this collection
@@ -247,9 +247,9 @@ protected:
 
     GeometryCollection* reverseImpl() const override;
 
-    GeometryCollection* getLinearizedImpl(double) const override;
+    GeometryCollection* getLinearizedImpl(const algorithm::CurveToLineParams&) const override;
 
-    GeometryCollection* getCurvedImpl(double) const override;
+    GeometryCollection* getCurvedImpl(const algorithm::LineToCurveParams&) const override;
 
     int
     getSortIndex() const override
diff --git a/include/geos/geom/LineString.h b/include/geos/geom/LineString.h
index ba3a391ee..8ec488f50 100644
--- a/include/geos/geom/LineString.h
+++ b/include/geos/geom/LineString.h
@@ -92,7 +92,7 @@ public:
 
     double getLength() const override;
 
-    std::unique_ptr<Curve> getCurved(double distanceTolerance) const;
+    std::unique_ptr<Curve> getCurved(const algorithm::LineToCurveParams&) const;
 
     bool isCurved() const override {
         return false;
@@ -127,9 +127,9 @@ protected:
     LineString(const std::shared_ptr<const CoordinateSequence> & pts,
                const GeometryFactory& newFactory);
 
-    LineString* getLinearizedImpl(double) const override { return cloneImpl(); }
+    LineString* getLinearizedImpl(const algorithm::CurveToLineParams&) const override { return cloneImpl(); }
 
-    Curve* getCurvedImpl(double distanceTolerance) const override;
+    Curve* getCurvedImpl(const algorithm::LineToCurveParams&) const override;
 
     LineString* cloneImpl() const override { return new LineString(*this); }
 
diff --git a/include/geos/geom/MultiCurve.h b/include/geos/geom/MultiCurve.h
index 028554ae3..868b24aa2 100644
--- a/include/geos/geom/MultiCurve.h
+++ b/include/geos/geom/MultiCurve.h
@@ -51,8 +51,8 @@ public:
 
     GeometryTypeId getGeometryTypeId() const override;
 
-    std::unique_ptr<MultiLineString> getLinearized(double distanceTolerance) const {
-        return std::unique_ptr<MultiLineString>(getLinearizedImpl(distanceTolerance));
+    std::unique_ptr<MultiLineString> getLinearized(const algorithm::CurveToLineParams& params) const {
+        return std::unique_ptr<MultiLineString>(getLinearizedImpl(params));
     }
 
     bool hasDimension(Dimension::DimensionType d) const override
@@ -119,9 +119,9 @@ protected:
 
     MultiCurve* reverseImpl() const override;
 
-    MultiCurve* getCurvedImpl(double) const override { return cloneImpl(); }
+    MultiCurve* getCurvedImpl(const algorithm::LineToCurveParams&) const override { return cloneImpl(); }
 
-    MultiLineString* getLinearizedImpl(double) const override;
+    MultiLineString* getLinearizedImpl(const algorithm::CurveToLineParams&) const override;
 
     int
     getSortIndex() const override
diff --git a/include/geos/geom/MultiLineString.h b/include/geos/geom/MultiLineString.h
index 2e4973981..6c2da99bc 100644
--- a/include/geos/geom/MultiLineString.h
+++ b/include/geos/geom/MultiLineString.h
@@ -81,7 +81,7 @@ public:
 
     GeometryTypeId getGeometryTypeId() const override;
 
-    std::unique_ptr<MultiLineString> getLinearized(double) const {
+    std::unique_ptr<MultiLineString> getLinearized(const algorithm::CurveToLineParams&) const {
         return clone();
     }
 
@@ -139,9 +139,9 @@ protected:
 
     MultiLineString* reverseImpl() const override;
 
-    GeometryCollection* getCurvedImpl(double disanceTolerance) const override;
+    GeometryCollection* getCurvedImpl(const algorithm::LineToCurveParams&) const override;
 
-    MultiLineString* getLinearizedImpl(double stepDegrees) const override;
+    MultiLineString* getLinearizedImpl(const algorithm::CurveToLineParams&) const override;
 
     int
     getSortIndex() const override
diff --git a/include/geos/geom/MultiPolygon.h b/include/geos/geom/MultiPolygon.h
index 991880d8d..049b4c6eb 100644
--- a/include/geos/geom/MultiPolygon.h
+++ b/include/geos/geom/MultiPolygon.h
@@ -131,9 +131,9 @@ protected:
 
     MultiPolygon* cloneImpl() const override { return new MultiPolygon(*this); }
 
-    GeometryCollection* getCurvedImpl(double distanceTolerance) const override;
+    GeometryCollection* getCurvedImpl(const algorithm::LineToCurveParams&) const override;
 
-    MultiPolygon* getLinearizedImpl(double) const override { return cloneImpl(); }
+    MultiPolygon* getLinearizedImpl(const algorithm::CurveToLineParams&) const override { return cloneImpl(); }
 
     MultiPolygon* reverseImpl() const override;
 
diff --git a/include/geos/geom/MultiSurface.h b/include/geos/geom/MultiSurface.h
index ed9ab49f5..754345fd1 100644
--- a/include/geos/geom/MultiSurface.h
+++ b/include/geos/geom/MultiSurface.h
@@ -49,8 +49,8 @@ public:
 
     GeometryTypeId getGeometryTypeId() const override;
 
-    std::unique_ptr<MultiPolygon> getLinearized(double degreeSpacing) const {
-        return std::unique_ptr<MultiPolygon>(getLinearizedImpl(degreeSpacing));
+    std::unique_ptr<MultiPolygon> getLinearized(const algorithm::CurveToLineParams& params) const {
+        return std::unique_ptr<MultiPolygon>(getLinearizedImpl(params));
     }
 
     bool hasDimension(Dimension::DimensionType d) const override
@@ -85,9 +85,9 @@ protected:
         return new MultiSurface(*this);
     }
 
-    MultiSurface* getCurvedImpl(double) const override { return cloneImpl(); }
+    MultiSurface* getCurvedImpl(const algorithm::LineToCurveParams&) const override { return cloneImpl(); }
 
-    MultiPolygon* getLinearizedImpl(double) const override;
+    MultiPolygon* getLinearizedImpl(const algorithm::CurveToLineParams&) const override;
 
     int
     getSortIndex() const override
diff --git a/include/geos/geom/Point.h b/include/geos/geom/Point.h
index 79fd3af96..0ccec2ac3 100644
--- a/include/geos/geom/Point.h
+++ b/include/geos/geom/Point.h
@@ -187,9 +187,9 @@ protected:
 
     Point(const Point& p);
 
-    Point* getCurvedImpl(double) const override { return cloneImpl(); };
+    Point* getCurvedImpl(const algorithm::LineToCurveParams&) const override { return cloneImpl(); };
 
-    Point* getLinearizedImpl(double) const override { return cloneImpl(); };
+    Point* getLinearizedImpl(const algorithm::CurveToLineParams&) const override { return cloneImpl(); };
 
     Point* cloneImpl() const override { return new Point(*this); }
 
diff --git a/include/geos/geom/Polygon.h b/include/geos/geom/Polygon.h
index b4499e514..99bcb242b 100644
--- a/include/geos/geom/Polygon.h
+++ b/include/geos/geom/Polygon.h
@@ -94,11 +94,11 @@ public:
     std::string getGeometryType() const override;
     GeometryTypeId getGeometryTypeId() const override;
 
-    std::unique_ptr<Surface> getCurved(double distanceTolerance) const {
-        return std::unique_ptr<Surface>(getCurvedImpl(distanceTolerance));
+    std::unique_ptr<Surface> getCurved(const algorithm::LineToCurveParams& params) const {
+        return std::unique_ptr<Surface>(getCurvedImpl(params));
     }
 
-    std::unique_ptr<Polygon> getLinearized(double) const;
+    std::unique_ptr<Polygon> getLinearized(const algorithm::CurveToLineParams&) const;
 
     void normalize() override;
 
@@ -122,9 +122,9 @@ protected:
 
     using SurfaceImpl::SurfaceImpl;
 
-    Surface* getCurvedImpl(double) const override;
+    Surface* getCurvedImpl(const algorithm::LineToCurveParams&) const override;
 
-    Polygon* getLinearizedImpl(double) const override { return cloneImpl(); };
+    Polygon* getLinearizedImpl(const algorithm::CurveToLineParams&) const override { return cloneImpl(); };
 
     Polygon* cloneImpl() const override { return new Polygon(*this); }
 
diff --git a/src/algorithm/CurveBuilder.cpp b/src/algorithm/CurveBuilder.cpp
index 25d696542..e5aa22877 100644
--- a/src/algorithm/CurveBuilder.cpp
+++ b/src/algorithm/CurveBuilder.cpp
@@ -21,6 +21,8 @@
 #include <geos/geom/GeometryFactory.h>
 #include <geos/geom/LineString.h>
 
+#include "geos/algorithm/LineToCurveParams.h"
+
 namespace geos::algorithm {
 
 using geom::CircularArc;
@@ -34,13 +36,13 @@ CurveBuilder::CurveBuilder(const GeometryFactory &p_factory)
     : factory(p_factory) {}
 
 std::unique_ptr<geom::Curve>
-CurveBuilder::getCurved(const LineString &ls, double distanceTolerance) {
+CurveBuilder::getCurved(const LineString &ls, const LineToCurveParams& params) {
     CurveBuilder cb(*ls.getFactory());
-    return cb.compute(ls, distanceTolerance);
+    return cb.compute(ls, params);
 }
 
 std::unique_ptr<geom::Curve>
-CurveBuilder::compute(const LineString& ls, double distanceTolerance) {
+CurveBuilder::compute(const LineString& ls, const LineToCurveParams& params) {
     const auto& points = *ls.getSharedCoordinates();
 
     std::size_t start = 0;
@@ -71,7 +73,7 @@ CurveBuilder::compute(const LineString& ls, double distanceTolerance) {
             const double distance = pt.distance(arc.getCenter());
 
             // Does the radius match?
-            if (std::abs(distance - arc.getRadius()) > distanceTolerance) {
+            if (std::abs(distance - arc.getRadius()) > params.getRadiusTolerance()) {
                 break;
             }
 
@@ -82,9 +84,7 @@ CurveBuilder::compute(const LineString& ls, double distanceTolerance) {
             const double prevAngle = Angle::angleBetween(arc.p0(), arc.p1(), arc.p2());
             const double currAngle = Angle::angleBetween(prev2, prev1, pt);
 
-            double angleTolerance = prevAngle*1e-3;
-
-            if (std::abs(currAngle - prevAngle) > angleTolerance) {
+            if (std::abs(currAngle - prevAngle) > prevAngle * params.getAngleStepTolerance()) {
                 break;
             }
 
diff --git a/src/geom/CircularArc.cpp b/src/geom/CircularArc.cpp
index 9449e42b3..c5022c612 100644
--- a/src/geom/CircularArc.cpp
+++ b/src/geom/CircularArc.cpp
@@ -12,8 +12,9 @@
  *
  **********************************************************************/
 
-#include <geos/algorithm/CircularArcs.h>
 #include <geos/algorithm/Angle.h>
+#include <geos/algorithm/CircularArcs.h>
+#include <geos/algorithm/CurveToLineParams.h>
 #include <geos/geom/CircularArc.h>
 #include <geos/geom/Envelope.h>
 #include <geos/triangulate/quadedge/TrianglePredicate.h>
@@ -348,13 +349,15 @@ CircularArc::getLength() const
 
 
 void
-CircularArc::addLinearizedPoints(CoordinateSequence& seq, double stepDegrees) const
+CircularArc::addLinearizedPoints(CoordinateSequence& seq, const algorithm::CurveToLineParams& params) const
 {
     if (isLinear()) {
         seq.add(*getCoordinateSequence(), getCoordinatePosition() + 1, getCoordinatePosition() + 2);
         return;
     }
 
+    const double stepDegrees = params.getStepSizeDegrees(*this);
+
     const double angle = getAngle();
     const bool isCCW = getOrientation() == geos::algorithm::Orientation::COUNTERCLOCKWISE;
     const double stepRad = stepDegrees * MATH_PI / 180.0;
diff --git a/src/geom/CircularString.cpp b/src/geom/CircularString.cpp
index 83d4ceeda..4eeb941de 100644
--- a/src/geom/CircularString.cpp
+++ b/src/geom/CircularString.cpp
@@ -154,14 +154,14 @@ CircularString::normalizeClosed() {
 }
 
 std::unique_ptr<Curve>
-CircularString::getCurved(double) const
+CircularString::getCurved(const algorithm::LineToCurveParams&) const
 {
     return getFactory()->createCircularString(points);
 }
 
 
 LineString*
-CircularString::getLinearizedImpl(double degreeSpacing) const {
+CircularString::getLinearizedImpl(const algorithm::CurveToLineParams& params) const {
     if (isEmpty()) {
         return getFactory()->createLineString().release();
     }
@@ -171,7 +171,7 @@ CircularString::getLinearizedImpl(double degreeSpacing) const {
 
     for (const CircularArc& arc : getArcs())
     {
-        arc.addLinearizedPoints(*seq, degreeSpacing);
+        arc.addLinearizedPoints(*seq, params);
     }
 
     return getFactory()->createLineString(seq).release();
diff --git a/src/geom/CompoundCurve.cpp b/src/geom/CompoundCurve.cpp
index 9d771474e..07ca9a57e 100644
--- a/src/geom/CompoundCurve.cpp
+++ b/src/geom/CompoundCurve.cpp
@@ -254,7 +254,7 @@ CompoundCurve::getLength() const
 }
 
 LineString*
-CompoundCurve::getLinearizedImpl(double degreeSpacing) const
+CompoundCurve::getLinearizedImpl(const algorithm::CurveToLineParams& params) const
 {
     auto seq = std::make_shared<CoordinateSequence>(0, hasZ(), hasM());
     for (const auto& curve : curves) {
@@ -268,7 +268,7 @@ CompoundCurve::getLinearizedImpl(double degreeSpacing) const
             for (std::size_t i = 0; i < curveSeq->size() - 2; i += 2)
             {
                 CircularArc arc(*curveSeq, i);
-                arc.addLinearizedPoints(*seq, degreeSpacing);
+                arc.addLinearizedPoints(*seq, params);
             }
         } else {
             seq->add(*curveSeq, 1, curveSeq->size() - 1);
diff --git a/src/geom/Curve.cpp b/src/geom/Curve.cpp
index 7a748f12b..f5eea9274 100644
--- a/src/geom/Curve.cpp
+++ b/src/geom/Curve.cpp
@@ -68,15 +68,15 @@ Curve::reverse() const {
 }
 
 std::unique_ptr<LineString>
-Curve::getLinearized(double degreeSpacing) const
+Curve::getLinearized(const algorithm::CurveToLineParams& params) const
 {
-    return std::unique_ptr<LineString>(detail::down_cast<LineString*>(getLinearizedImpl(degreeSpacing)));
+    return std::unique_ptr<LineString>(detail::down_cast<LineString*>(getLinearizedImpl(params)));
 }
 
 std::unique_ptr<Curve>
-Curve::getCurved(double distanceTolerance) const
+Curve::getCurved(const algorithm::LineToCurveParams& params) const
 {
-    return std::unique_ptr<Curve>(getCurvedImpl(distanceTolerance));
+    return std::unique_ptr<Curve>(getCurvedImpl(params));
 }
 
 
diff --git a/src/geom/CurvePolygon.cpp b/src/geom/CurvePolygon.cpp
index a9e34f369..4986f5622 100644
--- a/src/geom/CurvePolygon.cpp
+++ b/src/geom/CurvePolygon.cpp
@@ -22,6 +22,8 @@
 #include <geos/geom/SimpleCurve.h>
 #include <geos/util/UnsupportedOperationException.h>
 
+#include "geos/algorithm/CurveToLineParams.h"
+
 namespace geos {
 namespace geom {
 
@@ -102,15 +104,15 @@ namespace geom {
     }
 
     std::unique_ptr<Polygon>
-    CurvePolygon::getLinearized(double spacingDegrees) const {
-        return std::unique_ptr<Polygon>(getLinearizedImpl(spacingDegrees));
+    CurvePolygon::getLinearized(const algorithm::CurveToLineParams& params) const {
+        return std::unique_ptr<Polygon>(getLinearizedImpl(params));
     }
 
     Polygon*
-    CurvePolygon::getLinearizedImpl(double spacingDegrees) const {
+    CurvePolygon::getLinearizedImpl(const algorithm::CurveToLineParams& params) const {
         const auto& gfact = *getFactory();
 
-        auto linShell = gfact.createLinearRing(shell->getLinearized(spacingDegrees)->getSharedCoordinates());
+        auto linShell = gfact.createLinearRing(shell->getLinearized(params)->getSharedCoordinates());
 
         if (holes.empty()) {
             return getFactory()->createPolygon(std::move(linShell)).release();
@@ -118,7 +120,7 @@ namespace geom {
 
         std::vector<std::unique_ptr<LinearRing>> linHoles(holes.size());
         for (size_t i = 0; i < holes.size(); i++) {
-            linHoles[i] = gfact.createLinearRing(holes[i]->getLinearized(spacingDegrees)->getSharedCoordinates());
+            linHoles[i] = gfact.createLinearRing(holes[i]->getLinearized(params)->getSharedCoordinates());
         }
         return getFactory()->createPolygon(std::move(linShell), std::move(linHoles)).release();
     }
diff --git a/src/geom/GeometryCollection.cpp b/src/geom/GeometryCollection.cpp
index 9b61fbf86..8071260fe 100644
--- a/src/geom/GeometryCollection.cpp
+++ b/src/geom/GeometryCollection.cpp
@@ -31,6 +31,8 @@
 #include <vector>
 #include <memory>
 
+#include "geos/algorithm/CurveToLineParams.h"
+
 namespace geos {
 namespace geom { // geos::geom
 
@@ -108,11 +110,11 @@ GeometryCollection::getCoordinates() const
 }
 
 GeometryCollection*
-GeometryCollection::getCurvedImpl(double distanceTolerance) const
+GeometryCollection::getCurvedImpl(const algorithm::LineToCurveParams& params) const
 {
     std::vector<std::unique_ptr<Geometry>> curvedGeoms(geometries.size());
     for (std::size_t i = 0; i < geometries.size(); i++) {
-        curvedGeoms[i] = geometries[i]->getCurved(distanceTolerance);
+        curvedGeoms[i] = geometries[i]->getCurved(params);
     }
 
     return getFactory()->createGeometryCollection(std::move(curvedGeoms)).release();
@@ -120,11 +122,11 @@ GeometryCollection::getCurvedImpl(double distanceTolerance) const
 
 
 GeometryCollection*
-GeometryCollection::getLinearizedImpl(double stepDegrees) const
+GeometryCollection::getLinearizedImpl(const algorithm::CurveToLineParams& params) const
 {
     std::vector<std::unique_ptr<Geometry>> linGeoms(geometries.size());
     for (std::size_t i = 0; i < geometries.size(); i++) {
-        linGeoms[i] = geometries[i]->getLinearized(stepDegrees);
+        linGeoms[i] = geometries[i]->getLinearized(params);
     }
 
     return getFactory()->createGeometryCollection(std::move(linGeoms)).release();
diff --git a/src/geom/LineString.cpp b/src/geom/LineString.cpp
index e1eeb2f1d..ee04e0f3b 100644
--- a/src/geom/LineString.cpp
+++ b/src/geom/LineString.cpp
@@ -107,15 +107,15 @@ LineString::validateConstruction()
 }
 
 Curve*
-LineString::getCurvedImpl(double distanceTolerance) const
+LineString::getCurvedImpl(const algorithm::LineToCurveParams& params) const
 {
-    return getCurved(distanceTolerance).release();
+    return getCurved(params).release();
 }
 
 std::unique_ptr<Curve>
-LineString::getCurved(double distanceTolerance) const
+LineString::getCurved(const algorithm::LineToCurveParams& params) const
 {
-    return CurveBuilder::getCurved(*this, distanceTolerance);
+    return CurveBuilder::getCurved(*this, params);
 }
 
 std::string
diff --git a/src/geom/MultiCurve.cpp b/src/geom/MultiCurve.cpp
index ccdc7725f..136aa9e5e 100644
--- a/src/geom/MultiCurve.cpp
+++ b/src/geom/MultiCurve.cpp
@@ -79,12 +79,12 @@ MultiCurve::getGeometryTypeId() const
 }
 
 MultiLineString*
-MultiCurve::getLinearizedImpl(double stepSizeDegrees) const
+MultiCurve::getLinearizedImpl(const algorithm::CurveToLineParams& params) const
 {
     std::vector<std::unique_ptr<Geometry>> lines(geometries.size());
 
     for (std::size_t i = 0; i < geometries.size(); i++) {
-        lines[i] = geometries[i]->getLinearized(stepSizeDegrees);
+        lines[i] = geometries[i]->getLinearized(params);
     }
 
     return getFactory()->createMultiLineString(std::move(lines)).release();
diff --git a/src/geom/MultiLineString.cpp b/src/geom/MultiLineString.cpp
index be9f15b06..91d4f1860 100644
--- a/src/geom/MultiLineString.cpp
+++ b/src/geom/MultiLineString.cpp
@@ -27,6 +27,8 @@
 
 #include <vector>
 
+#include "geos/algorithm/LineToCurveParams.h"
+
 using geos::geom::GeometryFactory;
 using geos::geom::MultiLineString;
 using geos::geomgraph::GeometryGraph;
@@ -107,14 +109,14 @@ MultiLineString::getGeometryTypeId() const
 }
 
 GeometryCollection*
-MultiLineString::getCurvedImpl(double disanceTolerance) const
+MultiLineString::getCurvedImpl(const algorithm::LineToCurveParams& params) const
 {
     std::vector<std::unique_ptr<Geometry>> curvedGeoms(geometries.size());
 
     bool hasCurves = false;
 
     for (std::size_t i = 0; i < geometries.size(); i++) {
-        curvedGeoms[i] = detail::down_cast<Curve*>(geometries[i].get())->getCurved(disanceTolerance);
+        curvedGeoms[i] = detail::down_cast<Curve*>(geometries[i].get())->getCurved(params);
         hasCurves |= curvedGeoms[i]->hasCurvedComponents();
     }
 
@@ -126,7 +128,7 @@ MultiLineString::getCurvedImpl(double disanceTolerance) const
 }
 
 MultiLineString*
-MultiLineString::getLinearizedImpl(double) const
+MultiLineString::getLinearizedImpl(const algorithm::CurveToLineParams&) const
 {
     return cloneImpl();
 }
diff --git a/src/geom/MultiPolygon.cpp b/src/geom/MultiPolygon.cpp
index a4282df2d..ae5c253fc 100644
--- a/src/geom/MultiPolygon.cpp
+++ b/src/geom/MultiPolygon.cpp
@@ -89,13 +89,13 @@ MultiPolygon::getBoundary() const
 }
 
 GeometryCollection*
-MultiPolygon::getCurvedImpl(double distanceTolerance) const {
+MultiPolygon::getCurvedImpl(const algorithm::LineToCurveParams& params) const {
     std::vector<std::unique_ptr<Geometry>> curvedGeoms(geometries.size());
 
     bool hasCurves = false;
 
     for (std::size_t i = 0; i < geometries.size(); i++) {
-        curvedGeoms[i] = geometries[i]->getCurved(distanceTolerance);
+        curvedGeoms[i] = geometries[i]->getCurved(params);
         if (curvedGeoms[i]->hasCurvedComponents()) {
             hasCurves = true;
         }
diff --git a/src/geom/MultiSurface.cpp b/src/geom/MultiSurface.cpp
index f390e9958..1c1dc1570 100644
--- a/src/geom/MultiSurface.cpp
+++ b/src/geom/MultiSurface.cpp
@@ -18,6 +18,8 @@
 #include <geos/geom/MultiCurve.h>
 #include <geos/geom/MultiSurface.h>
 
+#include "geos/algorithm/CurveToLineParams.h"
+
 namespace geos {
 namespace geom {
 
@@ -87,12 +89,12 @@ MultiSurface::getGeometryTypeId() const
 }
 
 MultiPolygon*
-MultiSurface::getLinearizedImpl(double stepSizeDegrees) const
+MultiSurface::getLinearizedImpl(const algorithm::CurveToLineParams& params) const
 {
     std::vector<std::unique_ptr<Geometry>> polygons(geometries.size());
 
     for (std::size_t i = 0; i < geometries.size(); i++) {
-        polygons[i] = geometries[i]->getLinearized(stepSizeDegrees);
+        polygons[i] = geometries[i]->getLinearized(params);
     }
 
     return getFactory()->createMultiPolygon(std::move(polygons)).release();
diff --git a/src/geom/Polygon.cpp b/src/geom/Polygon.cpp
index b4e4864eb..806b3503f 100644
--- a/src/geom/Polygon.cpp
+++ b/src/geom/Polygon.cpp
@@ -37,6 +37,8 @@
 #include <algorithm>
 #include <memory>
 
+#include "geos/algorithm/CurveToLineParams.h"
+
 #ifndef GEOS_DEBUG
 #define GEOS_DEBUG 0
 #endif
@@ -68,9 +70,9 @@ Polygon::getCoordinates() const
 }
 
 Surface*
-Polygon::getCurvedImpl(double distanceTolerance) const
+Polygon::getCurvedImpl(const algorithm::LineToCurveParams& params) const
 {
-    auto curvedShell = shell->getCurved(distanceTolerance);
+    auto curvedShell = shell->getCurved(params);
     bool isCurved = curvedShell->hasCurvedComponents();
 
     if (holes.empty()) {
@@ -83,7 +85,7 @@ Polygon::getCurvedImpl(double distanceTolerance) const
 
     std::vector<std::unique_ptr<Curve>> holesCurved(holes.size());
     for (std::size_t i = 0; i < holes.size(); i++) {
-        holesCurved[i] = holes[i]->getCurved(distanceTolerance);
+        holesCurved[i] = holes[i]->getCurved(params);
         isCurved |= holesCurved[i]->hasCurvedComponents();
     }
 
@@ -95,9 +97,9 @@ Polygon::getCurvedImpl(double distanceTolerance) const
 }
 
 std::unique_ptr<Polygon>
-Polygon::getLinearized(double degreeSpacing) const
+Polygon::getLinearized(const algorithm::CurveToLineParams& params) const
 {
-    return std::unique_ptr<Polygon>(getLinearizedImpl(degreeSpacing));
+    return std::unique_ptr<Polygon>(getLinearizedImpl(params));
 }
 
 std::string
diff --git a/tests/unit/algorithm/CurveBuilderTest.cpp b/tests/unit/algorithm/CurveBuilderTest.cpp
index bb03a129a..08192c373 100644
--- a/tests/unit/algorithm/CurveBuilderTest.cpp
+++ b/tests/unit/algorithm/CurveBuilderTest.cpp
@@ -1,9 +1,13 @@
 #include <tut/tut.hpp>
 
+#include <geos/algorithm/CurveToLineParams.h>
+#include <geos/algorithm/LineToCurveParams.h>
 #include <geos/io/WKTReader.h>
 
 #include "utility.h"
 
+using geos::algorithm::CurveToLineParams;
+
 namespace tut {
 
 struct test_algorithm_curvebuilder_data {
@@ -12,8 +16,13 @@ struct test_algorithm_curvebuilder_data {
     void checkRoundTrip(const std::string& wkt_in, const std::string& wkt_expected, double stepSizeDegrees) const {
         auto inCurve = reader_.read(wkt_in);
 
-        auto linearized = inCurve->getLinearized(stepSizeDegrees);
-        auto outCurve = linearized->getCurved(1e-6);
+        auto ctlParams = CurveToLineParams::stepSizeDegrees(stepSizeDegrees);
+        auto linearized = inCurve->getLinearized(ctlParams);
+
+        auto ltcParams = geos::algorithm::LineToCurveParams();
+        ltcParams.setRadiusTolerance(1e-6);
+
+        auto outCurve = linearized->getCurved(ltcParams);
         auto expected = reader_.read(wkt_expected);
 
         double distanceTolerance = 1e-4;
@@ -27,16 +36,23 @@ struct test_algorithm_curvebuilder_data {
 
     void checkLineToCurve(const std::string& wkt_in, const std::string& wkt_expected, double distanceTolerance) const {
         auto ls = reader_.read<LineString>(wkt_in);
-        auto curve = ls->getCurved(distanceTolerance);
+
+        auto ltcParams = geos::algorithm::LineToCurveParams();
+        ltcParams.setRadiusTolerance(distanceTolerance);
+
+        auto curve = ls->getCurved(ltcParams);
         auto expected = reader_.read(wkt_expected);
 
         ensure_equals_exact_geometry_xyzm(curve.get(), expected.get(), distanceTolerance);
     }
 
     void checkLineToCurveUnchanged(const std::string& wkt_in, double distanceTolerance) const {
+        auto ltcParams = geos::algorithm::LineToCurveParams();
+        ltcParams.setRadiusTolerance(distanceTolerance);
+
         auto ls = reader_.read<LineString>(wkt_in);
         auto expected = ls->clone();
-        auto curve = ls->getCurved(distanceTolerance);
+        auto curve = ls->getCurved(ltcParams);
 
         ensure_equals_exact_geometry_xyzm(curve.get(), expected.get(), 0.0);
     }
@@ -208,13 +224,16 @@ void object::test<15>()
 
     auto cs = reader_.read("CIRCULARSTRING (-5 0, 0 5, 5 0)");
 
-    auto lin = cs->getLinearized(90.0 / 4);
+    auto lin = cs->getLinearized(CurveToLineParams::stepSizeDegrees(90.0 / 4));
     auto linRev = lin->reverse();
 
-    auto curveRev = linRev->getCurved(1e-4);
+    geos::algorithm::LineToCurveParams ltc;
+    ltc.setRadiusTolerance(1e-4);
+
+    auto curveRev = linRev->getCurved(ltc);
     auto curve1 = curveRev->reverse();
 
-    auto curve2 = lin->getCurved(1e-4);
+    auto curve2 = lin->getCurved(ltc);
 
     ensure_equals_exact_geometry_xyzm(curve1.get(), curve2.get(), 0);
 }
@@ -242,5 +261,15 @@ void object::test<16>()
     checkRoundTripUnchanged("CIRCULARSTRING (0 0 1 7, 1 1 2 9, 2 0 4 13)", 20);
 }
 
+template<>
+template<>
+void object::test<17>() {
+    set_test_name("big coordinates");
+
+    checkRoundTripUnchanged("CIRCULARSTRING (426857.987717275 5427937.52346616,500000.000000001 5538630.70286887,573142.012282726 5427937.52346616)", 4);
+
+    checkRoundTripUnchanged("CIRCULARSTRING (426858 5427938,500000 5538632,573142 5427938)", 4);
+}
+
 
 }
diff --git a/tests/unit/geom/CircularStringTest.cpp b/tests/unit/geom/CircularStringTest.cpp
index f5ab15d2d..32e693e91 100644
--- a/tests/unit/geom/CircularStringTest.cpp
+++ b/tests/unit/geom/CircularStringTest.cpp
@@ -1,6 +1,9 @@
 #include <tut/tut.hpp>
 #include <tut/tut_macros.hpp>
 
+#include <geos/algorithm/CurveToLineParams.h>
+#include <geos/algorithm/LineToCurveParams.h>
+#include <geos/geom/CircularArc.h>
 #include <geos/geom/CircularString.h>
 #include <geos/geom/GeometryFactory.h>
 #include <geos/geom/CoordinateSequence.h>
@@ -9,8 +12,9 @@
 #include <geos/util/UnsupportedOperationException.h>
 
 #include "utility.h"
-#include "geos/geom/CircularArc.h"
 
+using geos::algorithm::CurveToLineParams;
+using geos::algorithm::LineToCurveParams;
 using geos::geom::CoordinateSequence;
 using geos::geom::CircularString;
 using XY = geos::geom::CoordinateXY;
@@ -47,34 +51,31 @@ struct test_circularstring_data {
     }
 
     void checkLinearizeMaxDeviation(const std::string& wkt_in, const std::string& wkt_expected, double maxDeviation, double tol=1e-12) {
-        cs_ = wktreader_.read<CircularString>(wkt_in);
-        auto seq = cs_->getCoordinatesRO();
-        geos::geom::CircularArc arc(*seq, 0);
-        double radius = arc.getRadius();
-
-        double stepSizeDegrees = getDegreesFromSagitta(radius, maxDeviation);
-        checkLinearize(wkt_in, wkt_expected, stepSizeDegrees, tol);
+        auto ctl = CurveToLineParams::maxDeviation(maxDeviation);
+        checkLinearize(wkt_in, wkt_expected, ctl, tol);
     }
 
     void checkLinearize(const std::string& wkt_in, const std::string& wkt_expected, double stepSizeDegrees, double tol=1e-12) {
+        auto ctl = CurveToLineParams::stepSizeDegrees(stepSizeDegrees);
+        checkLinearize(wkt_in, wkt_expected, ctl, tol);
+    }
+
+    void checkLinearize(const std::string& wkt_in, const std::string& wkt_expected, const geos::algorithm::CurveToLineParams& params, double tol=1e-12) {
         cs_ = wktreader_.read<CircularString>(wkt_in);
 
-        auto ls = cs_->getLinearized(stepSizeDegrees);
+        auto ls = cs_->getLinearized(params);
 
         auto expected = wktreader_.read(wkt_expected);
 
         ensure_equals_exact_geometry_xyzm(ls.get(), expected.get(), tol);
 
         auto csRev = cs_->reverse();
-        auto lsRev = csRev->getLinearized(stepSizeDegrees);
+        auto lsRev = csRev->getLinearized(params);
         auto lsRevRev = lsRev->reverse();
 
         ensure_equals_exact_geometry_xyzm(lsRevRev.get(), expected.get(), tol);
     }
 
-    static double getDegreesFromSagitta(double radius, double sagitta) {
-        return std::acos(1 - sagitta / radius) * 360 / geos::MATH_PI;
-    }
 };
 
 typedef test_group<test_circularstring_data> group;
@@ -390,7 +391,7 @@ template<>
 void object::test<15>() {
     set_test_name("liblwgeom: 10 segments per quadrant - circular");
     auto cs = wktreader_.read<CircularString>("CIRCULARSTRING (0 0, 1 0, 0 0)");
-    auto ls = cs->getLinearized(90.0 / 10);
+    auto ls = cs->getLinearized(CurveToLineParams::stepSizeDegrees(90.0 / 10));
     ensure_equals(ls->getNumPoints(), 41u); // PostGIS test has 40, but this is incorrect
     const auto* seq = ls->getCoordinatesRO();
     seq->forEachSegment([](const auto& p0, const auto& p1) {
@@ -453,8 +454,8 @@ void object::test<22>() {
     auto cs = wktreader_.read("CIRCULARSTRING(71.96 -65.64,22.2 -18.52,20 50)");
     auto csRev = cs->reverse();
 
-    auto ls1 = cs->getLinearized(90.0 / 4);
-    auto ls2 = cs->reverse()->getLinearized(90.0 / 4)->reverse();
+    auto ls1 = cs->getLinearized(CurveToLineParams::stepSizeDegrees(90.0 / 4));
+    auto ls2 = cs->reverse()->getLinearized(CurveToLineParams::stepSizeDegrees(90.0 / 4))->reverse();
 
     ensure("Linearization of reversed CIRCULARSTRING is not direction neutral", ls1->equalsExact(ls2.get(), 0.0));
 }
@@ -486,13 +487,15 @@ void object::test<25>()
 
     auto cs = wktreader_.read<CircularString>("CIRCULARSTRING(0 0, 1 1, 2 0)");
 
+    const auto params = CurveToLineParams::stepSizeDegrees(45);
+
     // check that we return LineString* rather than Curve* or Geometry*
-    std::unique_ptr<LineString> linearized = cs->getLinearized(45);
+    std::unique_ptr<LineString> linearized = cs->getLinearized(params);
 
     ensure_equals("CircularString::getLinearized", linearized.get()->getGeometryTypeId(), geos::geom::GEOS_LINESTRING);
-    ensure_equals("SimpleCurve::getLinearized", static_cast<geos::geom::SimpleCurve*>(cs.get())->getLinearized(45)->getGeometryTypeId(), geos::geom::GEOS_LINESTRING);
-    ensure_equals("Curve::getLinearized", static_cast<geos::geom::Curve*>(cs.get())->getLinearized(45)->getGeometryTypeId(), geos::geom::GEOS_LINESTRING);
-    ensure_equals("Geometry::getLinearized", static_cast<Geometry*>(cs.get())->getLinearized(45)->getGeometryTypeId(), geos::geom::GEOS_LINESTRING);
+    ensure_equals("SimpleCurve::getLinearized", static_cast<geos::geom::SimpleCurve*>(cs.get())->getLinearized(params)->getGeometryTypeId(), geos::geom::GEOS_LINESTRING);
+    ensure_equals("Curve::getLinearized", static_cast<geos::geom::Curve*>(cs.get())->getLinearized(params)->getGeometryTypeId(), geos::geom::GEOS_LINESTRING);
+    ensure_equals("Geometry::getLinearized", static_cast<Geometry*>(cs.get())->getLinearized(params)->getGeometryTypeId(), geos::geom::GEOS_LINESTRING);
 }
 
 template<>
@@ -504,7 +507,7 @@ void object::test<26>()
     auto cs = wktreader_.read<CircularString>("CIRCULARSTRING(0 0, 1 1, 2 0)");
 
     // Check that we return Curve* rather than Geometry*
-    std::unique_ptr<geos::geom::Curve> curved = cs->getCurved(1000);
+    std::unique_ptr<geos::geom::Curve> curved = cs->getCurved(LineToCurveParams::getDefault());
 
     ensure_equals_exact_geometry_xyzm(curved.get(), cs.get(), 0);
 }
@@ -517,7 +520,7 @@ void object::test<27>()
     using XYZM = geos::geom::CoordinateXYZM;
 
     auto cs = wktreader_.read<CircularString>("CIRCULARSTRING ZM (0 5 4 5, 5 0 6 8, 0 -5 30 40)");
-    auto ls = cs->getLinearized(30);
+    auto ls = cs->getLinearized(CurveToLineParams::stepSizeDegrees(30));
 
     const CoordinateSequence* seq = ls->getCoordinatesRO();
 
diff --git a/tests/unit/geom/CompoundCurveTest.cpp b/tests/unit/geom/CompoundCurveTest.cpp
index 0a542dbde..e74610288 100644
--- a/tests/unit/geom/CompoundCurveTest.cpp
+++ b/tests/unit/geom/CompoundCurveTest.cpp
@@ -2,6 +2,7 @@
 #include <tut/tut.hpp>
 #include <tut/tut_macros.hpp>
 
+#include <geos/algorithm/CurveToLineParams.h>
 #include <geos/geom/Coordinate.h>
 #include <geos/geom/CoordinateFilter.h>
 #include <geos/geom/GeometryFactory.h>
@@ -13,7 +14,10 @@
 #include <geos/operation/valid/RepeatedPointTester.h>
 #include <geos/util.h>
 
+#include "geos/algorithm/LineToCurveParams.h"
 
+
+using geos::algorithm::CurveToLineParams;
 using geos::geom::CompoundCurve;
 using geos::geom::Curve;
 using geos::geom::CoordinateXY;
@@ -422,11 +426,11 @@ void object::test<12>()
     set_test_name("getLinearized()");
 
     auto cc = wktreader_.read("COMPOUNDCURVE (CIRCULARSTRING(-5 0, 0 5, 4 3, 5 0, 0 -5), (0 -5, -5 0))");
-    auto ls = cc->getLinearized(90.0 / 4);
+    auto ls = cc->getLinearized(CurveToLineParams::stepSizeDegrees(90.0 / 4));
 
     ensure_equals(ls->getGeometryTypeId(), geos::geom::GEOS_LINESTRING);
-    ensure_equals(static_cast<geos::geom::Curve*>(cc.get())->getLinearized(90.0 / 4)->getGeometryTypeId(), geos::geom::GEOS_LINESTRING);
-    ensure_equals(static_cast<CompoundCurve*>(ls.get())->getLinearized(90.0 / 4)->getGeometryTypeId(), geos::geom::GEOS_LINESTRING);
+    ensure_equals(static_cast<geos::geom::Curve*>(cc.get())->getLinearized(CurveToLineParams::stepSizeDegrees(90.0 / 4))->getGeometryTypeId(), geos::geom::GEOS_LINESTRING);
+    ensure_equals(static_cast<CompoundCurve*>(ls.get())->getLinearized(CurveToLineParams::stepSizeDegrees(90.0 / 4))->getGeometryTypeId(), geos::geom::GEOS_LINESTRING);
 
     geos::operation::valid::RepeatedPointTester rpt;
     ensure(!rpt.hasRepeatedPoint(ls.get()));
@@ -435,7 +439,7 @@ void object::test<12>()
     ensure_equals_exact_geometry(static_cast<Geometry*>(ls.get()), expected.get(), 1e-4);
 
     auto ccRev = cc->reverse();
-    auto lsRev = ccRev->getLinearized(90.0 / 4);
+    auto lsRev = ccRev->getLinearized(CurveToLineParams::stepSizeDegrees(90.0 / 4));
     ensure_equals_exact_geometry(static_cast<Geometry*>(lsRev->reverse().get()), expected.get(), 1e-4);
 }
 
@@ -446,7 +450,7 @@ void object::test<13>()
     set_test_name("getCurved()");
 
     // check that we return Curve* rather than Geometry*
-    std::unique_ptr<Curve> curved = cc_->getCurved(100.0);
+    std::unique_ptr<Curve> curved = cc_->getCurved(geos::algorithm::LineToCurveParams::getDefault());
 
     ensure_equals_exact_geometry_xyzm(curved.get(), cc_.get(), 0);
 }
diff --git a/tests/unit/geom/CurvePolygonTest.cpp b/tests/unit/geom/CurvePolygonTest.cpp
index 0c3ded6cd..41e603cce 100644
--- a/tests/unit/geom/CurvePolygonTest.cpp
+++ b/tests/unit/geom/CurvePolygonTest.cpp
@@ -2,6 +2,8 @@
 #include <tut/tut.hpp>
 #include <tut/tut_macros.hpp>
 
+#include <geos/algorithm/CurveToLineParams.h>
+#include <geos/algorithm/LineToCurveParams.h>
 #include <geos/geom/GeometryFactory.h>
 #include <geos/geom/CircularString.h>
 #include <geos/geom/CompoundCurve.h>
@@ -287,7 +289,7 @@ void object::test<7>()
     auto cp = wktreader_.read<CurvePolygon>("CURVEPOLYGON (COMPOUNDCURVE (CIRCULARSTRING (0 0, 2 0, 2 1, 2 3, 4 3), (4 3, 4 5, 1 4, 0 0)), CIRCULARSTRING (1.7 1, 1.4 0.4, 1.6 0.4, 1.6 0.5, 1.7 1))");
 
     // check that we return Polygon* rather than Geometry*
-    std::unique_ptr<Polygon> poly = cp->getLinearized(90.0 / 4);
+    std::unique_ptr<Polygon> poly = cp->getLinearized(geos::algorithm::CurveToLineParams::stepSizeDegrees(90.0 / 4));
 
     auto expected = wktreader_.read("POLYGON ((0 0, 0.2675 -0.3446, 0.6464 -0.5607, 1.0793 -0.6152, 1.5 -0.5, 1.8446 -0.2325, 2.0607 0.1464, 2.1152 0.5793, 2 1, 1.6934 1.4588, 1.5858 2, 1.6934 2.5412, 2 3, 2.4588 3.3066, 3 3.4142, 3.5412 3.3066, 4 3, 4 5, 1 4, 0 0), (1.7 1, 1.5871 1.0537, 1.4623 1.0629, 1.3427 1.0265, 1.2444 0.9492, 1.1806 0.8416, 1.16 0.7183, 1.1855 0.5958, 1.2534 0.4908, 1.3548 0.4175, 1.4757 0.3858, 1.6 0.4, 1.6203 0.705, 1.7 1))");
 
@@ -300,7 +302,7 @@ void object::test<8>()
 {
     set_test_name("getCurved");
 
-    std::unique_ptr<CurvePolygon> curved = cp_->getCurved(1000);
+    std::unique_ptr<CurvePolygon> curved = cp_->getCurved(geos::algorithm::LineToCurveParams::getDefault());
 
     ensure_equals_exact_geometry_xyzm(curved.get(), cp_.get(), 0);
 }
diff --git a/tests/unit/geom/GeometryCollectionTest.cpp b/tests/unit/geom/GeometryCollectionTest.cpp
index c144059ac..5a847b913 100644
--- a/tests/unit/geom/GeometryCollectionTest.cpp
+++ b/tests/unit/geom/GeometryCollectionTest.cpp
@@ -6,6 +6,11 @@
 
 #include <memory>
 
+#include <geos/algorithm/CurveToLineParams.h>
+#include <geos/algorithm/LineToCurveParams.h>
+
+using geos::algorithm::CurveToLineParams;
+using geos::algorithm::LineToCurveParams;
 
 namespace tut {
 //
@@ -188,7 +193,7 @@ void object::test<10>() {
 
     auto gc = readWKT("GEOMETRYCOLLECTION(POINT (3 7), CIRCULARSTRING (0 0, 1 1, 2 0), LINESTRING (1 1, 2 3))");
 
-    auto linearized = gc->getLinearized(45);
+    auto linearized = gc->getLinearized(CurveToLineParams::stepSizeDegrees(45));
 
     auto expected = readWKT("GEOMETRYCOLLECTION(POINT (3 7), LINESTRING (0 0, 0.292893 0.707107, 1 1, 1.707107 0.707107, 2 0), LINESTRING (1 1, 2 3))");
 
@@ -202,7 +207,10 @@ void object::test<11>() {
 
     auto gc = readWKT("GEOMETRYCOLLECTION(POINT (3 7), LINESTRING (0 0, 0.292893 0.707107, 1 1, 1.707107 0.707107, 2 0), LINESTRING (1 1, 2 3))");
 
-    auto curved = gc->getCurved(1e-3);
+    auto params = LineToCurveParams::getDefault();
+    params.setRadiusTolerance(1e-3);
+
+    auto curved = gc->getCurved(params);
 
     auto expected = readWKT("GEOMETRYCOLLECTION(POINT (3 7), CIRCULARSTRING (0 0, 1 1, 2 0), LINESTRING (1 1, 2 3))");
 
diff --git a/tests/unit/geom/LineStringTest.cpp b/tests/unit/geom/LineStringTest.cpp
index 90edafcb3..20b2e7454 100644
--- a/tests/unit/geom/LineStringTest.cpp
+++ b/tests/unit/geom/LineStringTest.cpp
@@ -5,6 +5,8 @@
 #include <tut/tut.hpp>
 #include <utility.h>
 // geos
+#include <geos/algorithm/CurveToLineParams.h>
+#include <geos/algorithm/LineToCurveParams.h>
 #include <geos/geom/LineString.h>
 #include <geos/geom/Coordinate.h>
 #include <geos/geom/CoordinateFilter.h>
@@ -650,7 +652,7 @@ void object::test<36>()
     set_test_name("getLinearized");
 
     // check that we return LineString* rather than Curve* or Geometry*
-    std::unique_ptr<LineString> linearized = line_->getLinearized(1e-3);
+    std::unique_ptr<LineString> linearized = line_->getLinearized(geos::algorithm::CurveToLineParams::stepSizeDegrees(45));
 
     ensure_equals_exact_geometry_xyzm(linearized.get(), line_.get(), 0);
 }
@@ -666,7 +668,10 @@ void object::test<37>()
     auto input = reader.read<LineString>("LINESTRING (2 2, 2.292893 2.707107, 3 3, 3.707107 2.707107, 4 2, 2 2)");
 
     // check that we return Curve* instead of Geometry*
-    std::unique_ptr<geos::geom::Curve> curved = input->getCurved(1e-3);
+    auto params = geos::algorithm::LineToCurveParams::getDefault();
+    params.setRadiusTolerance(1e-3);
+
+    std::unique_ptr<geos::geom::Curve> curved = input->getCurved(params);
 
     ensure_equals(curved->getGeometryTypeId(), geos::geom::GEOS_COMPOUNDCURVE);
 
diff --git a/tests/unit/geom/MultiCurveTest.cpp b/tests/unit/geom/MultiCurveTest.cpp
index 316c22b5d..d85699f74 100644
--- a/tests/unit/geom/MultiCurveTest.cpp
+++ b/tests/unit/geom/MultiCurveTest.cpp
@@ -1,6 +1,8 @@
 #include <tut/tut.hpp>
 #include <tut/tut_macros.hpp>
 
+#include <geos/algorithm/CurveToLineParams.h>
+#include <geos/algorithm/LineToCurveParams.h>
 #include <geos/geom/GeometryFactory.h>
 #include <geos/geom/CompoundCurve.h>
 #include <geos/geom/CircularString.h>
@@ -210,7 +212,7 @@ void object::test<5>()
     set_test_name("getLinearized()");
 
     // check that we return MultiLineString*, not Geometry*
-    std::unique_ptr<geos::geom::MultiLineString> mls = mc_->getLinearized(2);
+    std::unique_ptr<geos::geom::MultiLineString> mls = mc_->getLinearized(geos::algorithm::CurveToLineParams::stepSizeDegrees(2));
 
     ensure_equals(mls->getGeometryTypeId(), geos::geom::GEOS_MULTILINESTRING);
     ensure_equals("getLength()", mls->getLength(), mc_->getLength(), 1e-3);
@@ -222,7 +224,7 @@ void object::test<6>()
 {
     set_test_name("getCurved()");
 
-    std::unique_ptr<geos::geom::GeometryCollection> curved = mc_->getCurved(100);
+    std::unique_ptr<geos::geom::GeometryCollection> curved = mc_->getCurved(geos::algorithm::LineToCurveParams::getDefault());
     ensure_equals_exact_geometry_xyzm(mc_.get(), curved.get(), 0);
 }
 
diff --git a/tests/unit/geom/MultiLineStringTest.cpp b/tests/unit/geom/MultiLineStringTest.cpp
index 361e884fd..258f80b39 100644
--- a/tests/unit/geom/MultiLineStringTest.cpp
+++ b/tests/unit/geom/MultiLineStringTest.cpp
@@ -3,6 +3,8 @@
 
 #include <tut/tut.hpp>
 // geos
+#include <geos/algorithm/CurveToLineParams.h>
+#include <geos/algorithm/LineToCurveParams.h>
 #include <geos/geom/GeometryFactory.h>
 #include <geos/geom/MultiLineString.h>
 #include <geos/io/WKTReader.h>
@@ -86,7 +88,7 @@ void object::test<5>()
     set_test_name("getLinearized()");
 
     // check that return MultiLineString*, not Geometry*
-    std::unique_ptr<MultiLineString> linearized = mls_->getLinearized(1.0);
+    std::unique_ptr<MultiLineString> linearized = mls_->getLinearized(geos::algorithm::CurveToLineParams::stepSizeDegrees(1));
 
     ensure_equals_exact_geometry_xyzm(linearized.get(), mls_.get(), 0);
 }
@@ -99,11 +101,16 @@ void object::test<6>()
 
     auto input = reader_.read<MultiLineString>("MULTILINESTRING ((3 3, 4 4), (-2 0, -1.414 1.414, 0 2, 1.414 1.414, 2 0, 2 3))");
 
-    ensure_equals(input->getCurved(1e-9)->getGeometryTypeId(), geos::geom::GEOS_MULTILINESTRING);
+    auto params = geos::algorithm::LineToCurveParams::getDefault();
 
+    // tolerance is too fine to generate a MultiCurve
+    params.setRadiusTolerance(1e-9);
     // check that we return GeometryCollection*, not Geometry*
-    std::unique_ptr<geos::geom::GeometryCollection> curved = input->getCurved(2e-3);
+    std::unique_ptr<geos::geom::GeometryCollection> curved = input->getCurved(params);
+    ensure_equals(curved->getGeometryTypeId(), geos::geom::GEOS_MULTILINESTRING);
 
+    params.setRadiusTolerance(2e-3);
+    curved = input->getCurved(params);
     ensure_equals(curved->getGeometryTypeId(), geos::geom::GEOS_MULTICURVE);
 
     auto expected = reader_.read("MULTICURVE ((3 3, 4 4), COMPOUNDCURVE(CIRCULARSTRING (-2 0, 0 2, 2 0), (2 0, 2 3)))");
diff --git a/tests/unit/geom/MultiPolygonTest.cpp b/tests/unit/geom/MultiPolygonTest.cpp
index df437bda1..9789ba6dc 100644
--- a/tests/unit/geom/MultiPolygonTest.cpp
+++ b/tests/unit/geom/MultiPolygonTest.cpp
@@ -3,6 +3,8 @@
 
 #include <tut/tut.hpp>
 // geos
+#include <geos/algorithm/CurveToLineParams.h>
+#include <geos/algorithm/LineToCurveParams.h>
 #include <geos/geom/GeometryFactory.h>
 #include <geos/geom/MultiPolygon.h>
 #include <geos/io/WKTReader.h>
@@ -81,7 +83,7 @@ void object::test<5>()
 {
     set_test_name("correct type returned by getLinearized");
 
-    ensure(mp_->getLinearized(0)->getGeometryTypeId() == geos::geom::GeometryTypeId::GEOS_MULTIPOLYGON);
+    ensure(mp_->getLinearized(geos::algorithm::CurveToLineParams::stepSizeDegrees(45))->getGeometryTypeId() == geos::geom::GeometryTypeId::GEOS_MULTIPOLYGON);
 }
 
 template<>
@@ -94,10 +96,14 @@ void object::test<6>()
         "MULTIPOLYGON (((0 0, 10 0, 10 10, 0 10, 0 0), (2 2, 2.292893 2.707107, 3 3, 3.707107 2.707107, 4 2, 2 2)),"
                                      "((20 0, 30 0, 30 10, 20 0)))");
 
-    // Tolerance to small to allow conversion to MultiSurface
-    ensure_equals(input->getCurved(1e-12)->getGeometryTypeId(), geos::geom::GeometryTypeId::GEOS_MULTIPOLYGON);
+    auto params = geos::algorithm::LineToCurveParams::getDefault();
 
-    auto curved = input->getCurved(0.001);
+    // Tolerance to small to allow conversion to MultiSurface
+    params.setRadiusTolerance(1e-12);
+    ensure_equals(input->getCurved(params)->getGeometryTypeId(), geos::geom::GeometryTypeId::GEOS_MULTIPOLYGON);
+
+    params.setRadiusTolerance(1e-3);
+    auto curved = input->getCurved(params);
 
     ensure_equals(curved->getGeometryTypeId(), geos::geom::GeometryTypeId::GEOS_MULTISURFACE);
 
diff --git a/tests/unit/geom/MultiSurfaceTest.cpp b/tests/unit/geom/MultiSurfaceTest.cpp
index 61abec09f..0e32439a1 100644
--- a/tests/unit/geom/MultiSurfaceTest.cpp
+++ b/tests/unit/geom/MultiSurfaceTest.cpp
@@ -1,6 +1,8 @@
 #include <tut/tut.hpp>
 #include <tut/tut_macros.hpp>
 
+#include <geos/algorithm/CurveToLineParams.h>
+#include <geos/algorithm/LineToCurveParams.h>
 #include <geos/geom/CircularString.h>
 #include <geos/geom/CurvePolygon.h>
 #include <geos/geom/GeometryFactory.h>
@@ -181,7 +183,7 @@ void object::test<4>()
     set_test_name("getLinearized()");
 
     // check that we return MultiPolygon*
-    std::unique_ptr<geos::geom::MultiPolygon> mp_ = ms_->getLinearized(2);
+    std::unique_ptr<geos::geom::MultiPolygon> mp_ = ms_->getLinearized(geos::algorithm::CurveToLineParams::stepSizeDegrees(4));
 
     ensure_equals(mp_->getGeometryTypeId(), geos::geom::GEOS_MULTIPOLYGON);
 }
@@ -192,7 +194,7 @@ void object::test<5>()
 {
     set_test_name("getCurved()");
 
-    std::unique_ptr<geos::geom::GeometryCollection> curved = ms_->getCurved(100);
+    std::unique_ptr<geos::geom::GeometryCollection> curved = ms_->getCurved(geos::algorithm::LineToCurveParams::getDefault());
 
     ensure_equals_exact_geometry_xyzm(ms_.get(), curved.get(), 0);
 }
diff --git a/tests/unit/geom/PointTest.cpp b/tests/unit/geom/PointTest.cpp
index d9720c094..26bcf2858 100644
--- a/tests/unit/geom/PointTest.cpp
+++ b/tests/unit/geom/PointTest.cpp
@@ -3,6 +3,8 @@
 
 #include <tut/tut.hpp>
 // geos
+#include <geos/algorithm/CurveToLineParams.h>
+#include <geos/algorithm/LineToCurveParams.h>
 #include <geos/geom/Point.h>
 #include <geos/geom/Coordinate.h>
 #include <geos/geom/CoordinateSequence.h>
@@ -638,7 +640,7 @@ void object::test<49>()
 {
     set_test_name("getLinearized");
 
-    ensure_equals_exact_geometry_xyzm(point_.get(), point_->getLinearized(4).get(), 0);
+    ensure_equals_exact_geometry_xyzm(point_.get(), point_->getLinearized(geos::algorithm::CurveToLineParams::stepSizeDegrees(4)).get(), 0);
 }
 
 template<>
@@ -647,7 +649,7 @@ void object::test<50>()
 {
     set_test_name("getCurved");
 
-    ensure_equals_exact_geometry_xyzm(point_.get(), point_->getCurved(1e-3).get(), 0);
+    ensure_equals_exact_geometry_xyzm(point_.get(), point_->getCurved(geos::algorithm::LineToCurveParams::getDefault()).get(), 0);
 }
 
 } // namespace tut
diff --git a/tests/unit/geom/PolygonTest.cpp b/tests/unit/geom/PolygonTest.cpp
index e57d33638..eee67405e 100644
--- a/tests/unit/geom/PolygonTest.cpp
+++ b/tests/unit/geom/PolygonTest.cpp
@@ -4,6 +4,8 @@
 #include <tut/tut.hpp>
 #include <utility.h>
 // geos
+#include <geos/algorithm/CurveToLineParams.h>
+#include <geos/algorithm/LineToCurveParams.h>
 #include <geos/geom/Polygon.h>
 #include <geos/geom/Coordinate.h>
 #include <geos/geom/CoordinateSequence.h>
@@ -755,7 +757,7 @@ void object::test<49>()
     set_test_name("getLinearized");
 
     // check that we return Polygon* rather than Geometry*
-    std::unique_ptr<Polygon> linearized = poly_zm_->getLinearized(4);
+    std::unique_ptr<Polygon> linearized = poly_zm_->getLinearized(geos::algorithm::CurveToLineParams::stepSizeDegrees(4));
 
     ensure_equals_exact_geometry_xyzm(linearized.get(), poly_zm_.get(),  0);
 }
@@ -770,7 +772,10 @@ void object::test<50>()
     auto input = reader.read<Polygon>("POLYGON ((0 0, 0.2675 -0.3446, 0.6464 -0.5607, 1.0793 -0.6152, 1.5 -0.5, 1.8446 -0.2325, 2.0607 0.1464, 2.1152 0.5793, 2 1, 1.6934 1.4588, 1.5858 2, 1.6934 2.5412, 2 3, 2.4588 3.3066, 3 3.4142, 3.5412 3.3066, 4 3, 4 5, 1 4, 0 0), (1.7 1, 1.5871 1.0537, 1.4623 1.0629, 1.3427 1.0265, 1.2444 0.9492, 1.1806 0.8416, 1.16 0.7183, 1.1855 0.5958, 1.2534 0.4908, 1.3548 0.4175, 1.4757 0.3858, 1.6 0.4, 1.6203 0.705, 1.7 1))");
 
     // check that we return Surface* rather than Geometry*
-    std::unique_ptr<geos::geom::Surface> curved = input->getCurved(0.01);
+    auto params = geos::algorithm::LineToCurveParams::getDefault();
+    params.setRadiusTolerance(0.01);
+
+    std::unique_ptr<geos::geom::Surface> curved = input->getCurved(params);
 
     ensure_equals(curved->getGeometryTypeId(), geos::geom::GeometryTypeId::GEOS_CURVEPOLYGON);
 

commit 99a971a9c14f17964fac94956c88ba8c31053d37
Author: Daniel Baston <dbaston at gmail.com>
Date:   Fri Jan 30 13:35:50 2026 -0500

    CurveBuilder: Perform round-trip from linearized arcs where Z/M vary unevenly

diff --git a/src/algorithm/CurveBuilder.cpp b/src/algorithm/CurveBuilder.cpp
index fab9b0cb0..25d696542 100644
--- a/src/algorithm/CurveBuilder.cpp
+++ b/src/algorithm/CurveBuilder.cpp
@@ -171,11 +171,37 @@ CurveBuilder::addArc(const CircularArc& arc, std::size_t stop) {
     const double averageRadius = 0.5*(p0.distance(averageCenter) + p2.distance(averageCenter));
 
     p1 = CircularArcs::getMidpoint(p0, p2, averageCenter, averageRadius, arc.isCCW());
-    p1.z = 0.5*(p0.z + p2.z);
-    p1.m = 0.5*(p0.m + p2.m);
+
+    if (points.hasZ() || points.hasM()) {
+        auto nPoints = stop - start + 1;
+
+        if (nPoints % 2) {
+            // We have an odd number of vertices, so the central vertex should be the same
+            // as the control point in the original arc.
+            auto midpointIndex = start + nPoints / 2;
+            p1.z = points.getZ(midpointIndex);
+            p1.m = points.getM(midpointIndex);
+        } else {
+            // We have an even number of vertices. Calculate Z/M of the control
+            // point in the original arc.
+            CoordinateXYZM a, b;
+            points.getAt(start + nPoints / 2 - 1, a);
+            points.getAt(start + nPoints / 2, b);
+
+            const double thetaA = CircularArcs::getAngle(a, averageCenter);
+            const double theta0 = CircularArcs::getAngle(p0, averageCenter);
+            const double theta1 = CircularArcs::getAngle(p1, averageCenter);
+
+            // Assumption: point a has the same angle fraction over [p0, p1] that b has over [p2, p1]
+            const double f = arc.isCCW() ? Angle::fractionCCW(thetaA, theta0, theta1) : 1 - Angle::fractionCCW(thetaA, theta1, theta0);
+
+            p1.z = (a.z + b.z - p0.z*(1 - f) - p2.z*(1 - f)) / (2 * f);
+            p1.m = (a.m + b.m - p0.m*(1 - f) - p2.m*(1 - f)) / (2 * f);
+        }
+    }
 
     if (!arcCoords) {
-        arcCoords = std::make_shared<CoordinateSequence>( points.hasZ(), points.hasM());
+        arcCoords = std::make_shared<CoordinateSequence>(0,  points.hasZ(), points.hasM());
         arcCoords->reserve(3);
         arcCoords->add(p0);
     }
@@ -184,4 +210,4 @@ CurveBuilder::addArc(const CircularArc& arc, std::size_t stop) {
     arcCoords->add(p2);
 }
 
-}
\ No newline at end of file
+}
diff --git a/tests/unit/algorithm/CurveBuilderTest.cpp b/tests/unit/algorithm/CurveBuilderTest.cpp
index 5b694e42c..bb03a129a 100644
--- a/tests/unit/algorithm/CurveBuilderTest.cpp
+++ b/tests/unit/algorithm/CurveBuilderTest.cpp
@@ -219,5 +219,28 @@ void object::test<15>()
     ensure_equals_exact_geometry_xyzm(curve1.get(), curve2.get(), 0);
 }
 
+template<>
+template<>
+void object::test<16>()
+{
+    set_test_name("Z/M preserved in round-trip");
+
+    // Midpoint Z/M halfway between start/end
+    // Linearized arc has an odd number of vertices
+    checkRoundTripUnchanged("CIRCULARSTRING (0 0 1 7, 1 1 2 9, 2 0 3 11)", 22.5);
+
+    // Midpoint Z/M not halfway between start/end
+    // Linearized arc has an odd number of vertices
+    checkRoundTripUnchanged("CIRCULARSTRING (0 0 1 7, 1 1 2 9, 2 0 4 13)", 22.5);
+
+    // Midpoint Z/M halfway between start/end
+    // Linearized arc has an even number of vertices
+    checkRoundTripUnchanged("CIRCULARSTRING (0 0 1 7, 1 1 2 9, 2 0 3 11)", 20);
+
+    // Midpoint Z/M not halfway between start/end
+    // Linearized arc has an odd number of vertices
+    checkRoundTripUnchanged("CIRCULARSTRING (0 0 1 7, 1 1 2 9, 2 0 4 13)", 20);
+}
+
 
 }

commit 6a74dc8d8ef6126d05bb3430c9644918e54484cd
Author: Daniel Baston <dbaston at gmail.com>
Date:   Fri Jan 30 12:14:23 2026 -0500

    CircularString::getLinearized: Match PostGIS behavior, considering control point in interpolation

diff --git a/include/geos/geom/CircularArc.h b/include/geos/geom/CircularArc.h
index bef115138..cdb50362a 100644
--- a/include/geos/geom/CircularArc.h
+++ b/include/geos/geom/CircularArc.h
@@ -153,6 +153,11 @@ public:
         return algorithm::Distance::pointToSegment(midpoint, p0(), p2());
     }
 
+    /// Interpolate Z/M values from the three points that define the arc.
+    /// Interpolation will be performed between [p0, p1] or [p1, p2] depending on
+    /// the position of the supplied point.
+    void interpolateZM(const CoordinateXY& pt, double& z, double& m) const;
+
     bool isCCW() const {
         return getOrientation() == algorithm::Orientation::COUNTERCLOCKWISE;
     }
diff --git a/include/geos/geom/MultiCurve.h b/include/geos/geom/MultiCurve.h
index d4ad24222..028554ae3 100644
--- a/include/geos/geom/MultiCurve.h
+++ b/include/geos/geom/MultiCurve.h
@@ -16,6 +16,7 @@
 
 #include <geos/geom/Curve.h>
 #include <geos/geom/GeometryCollection.h>
+#include <geos/geom/MultiLineString.h>
 
 namespace geos {
 namespace geom {
diff --git a/src/geom/CircularArc.cpp b/src/geom/CircularArc.cpp
index 561ab4671..9449e42b3 100644
--- a/src/geom/CircularArc.cpp
+++ b/src/geom/CircularArc.cpp
@@ -13,6 +13,7 @@
  **********************************************************************/
 
 #include <geos/algorithm/CircularArcs.h>
+#include <geos/algorithm/Angle.h>
 #include <geos/geom/CircularArc.h>
 #include <geos/geom/Envelope.h>
 #include <geos/triangulate/quadedge/TrianglePredicate.h>
@@ -372,28 +373,123 @@ CircularArc::addLinearizedPoints(CoordinateSequence& seq, double stepDegrees) co
     const bool hasZ = getCoordinateSequence()->hasZ();
     const bool hasM = getCoordinateSequence()->hasM();
 
-    CoordinateXYZM p0, p2;
-    getCoordinateSequence()->getAt(getCoordinatePosition(), p0);
-    getCoordinateSequence()->getAt(getCoordinatePosition() + 2, p2);
-
     for (int i = 1; i < nSegments; i++) {
         const int j = isCCW ? i: nSegments - i;
-        const double frac = static_cast<double>(j) / nSegments;
         const double theta = startAngle + j*adjStepRad;
 
         CoordinateXYZM pt{geos::algorithm::CircularArcs::createPoint(center, radius, theta)};
 
-        if (hasZ) {
-            pt.z = p0.z + frac*(p2.z - p0.z);
-        }
-        if (hasM) {
-            pt.m = p0.m + frac*(p2.m - p0.m);
+        if (hasZ || hasM) {
+            interpolateZM(pt, pt.z, pt.m);
         }
 
         seq.add(pt);
     }
 
-    seq.add(p2);
+    seq.add(*getCoordinateSequence(), getCoordinatePosition() + 2, getCoordinatePosition() + 2);
+}
+
+static double
+interpolateValue(double a1, double a2, double frac)
+{
+    frac = std::clamp(frac, 0.0, 1.0);
+    if (std::isnan(a1)) {
+        return a2;
+    }
+    if (std::isnan(a2)) {
+        return a1;
+    }
+    return a1 + frac * (a2 - a1);
+}
+
+void
+CircularArc::interpolateZM(const CoordinateXY &pt, double &z, double &m) const
+{
+    using geom::Ordinate;
+
+    const CoordinateSequence& seq = *getCoordinateSequence();
+    std::size_t i0 = getCoordinatePosition();
+
+    // Read Z, M from control point
+    double z1, m1;
+    seq.applyAt(i0 + 1, [&z1, &m1](const auto& arcPt) {
+        z1 = arcPt.template get<Ordinate::Z>();
+        m1 = arcPt.template get<Ordinate::M>();
+    });
+    // Test point = control point?
+    // Take Z, M from the control point
+    if (p1().equals2D(pt)) {
+        z = z1;
+        m = m1;
+        return;
+    }
+
+    // Read Z, M from start point
+    double z0, m0;
+    seq.applyAt(i0, [&z0, &m0](const auto& arcPt) {
+        z0 = arcPt.template get<Ordinate::Z>();
+        m0 = arcPt.template get<Ordinate::M>();
+    });
+    // Test point = start point?
+    // Take Z, M from the start point
+    if (p0().equals2D(pt)) {
+        z = z0;
+        m = m0;
+        return;
+    }
+
+    // Read Z, M from end point
+    double z2, m2;
+    seq.applyAt(i0 + 2, [&z2, &m2](const auto& arcPt) {
+        z2 = arcPt.template get<Ordinate::Z>();
+        m2 = arcPt.template get<Ordinate::M>();
+    });
+    // Test point = end point?
+    // Take Z, M from the end point
+    if (p2().equals2D(pt)) {
+        z = z2;
+        m = m2;
+        return;
+    }
+
+    double norm_theta0 = theta0();
+    const double norm_theta1 = theta1();
+    double norm_theta2 = theta2();
+    const double theta = algorithm::CircularArcs::getAngle(pt, getCenter());
+
+    if (!isCCW()) {
+        std::swap(norm_theta0, norm_theta2);
+        std::swap(z0, z2);
+        std::swap(m0, m2);
+    }
+
+    if (std::isnan(z1)) {
+        // Interpolate between p0 /  p2
+        const double frac = algorithm::Angle::fractionCCW(theta, norm_theta0, norm_theta2);
+        z = interpolateValue(z0, z2, frac);
+    } else if (algorithm::Angle::isWithinCCW(theta, norm_theta0, norm_theta1)) {
+        // Interpolate between p0 / p1
+        const double frac = algorithm::Angle::fractionCCW(theta, norm_theta0, norm_theta1);
+        z = interpolateValue(z0, z1, frac);
+    } else {
+        // Interpolate between p1 / p2
+        const double frac = algorithm::Angle::fractionCCW(theta, norm_theta1, norm_theta2);
+        z = interpolateValue(z1, z2, frac);
+    }
+
+    if (std::isnan(m1)) {
+        // Interpolate between p0 /  p2
+        const double frac = algorithm::Angle::fractionCCW(theta, norm_theta0, norm_theta2);
+        m = interpolateValue(m0, m2, frac);
+    } else if (algorithm::Angle::isWithinCCW(theta, norm_theta0, norm_theta1)) {
+        // Interpolate between p0 / p1
+        const double frac = algorithm::Angle::fractionCCW(theta, norm_theta0, norm_theta1);
+        m = interpolateValue(m0, m1, frac);
+    } else {
+        // Interpolate between p1 / p2
+        const double frac = algorithm::Angle::fractionCCW(theta, norm_theta1, norm_theta2);
+        m = interpolateValue(m1, m2, frac);
+    }
 }
 
 bool
diff --git a/tests/unit/algorithm/CurveBuilderTest.cpp b/tests/unit/algorithm/CurveBuilderTest.cpp
index 10bdd6f7d..5b694e42c 100644
--- a/tests/unit/algorithm/CurveBuilderTest.cpp
+++ b/tests/unit/algorithm/CurveBuilderTest.cpp
@@ -6,7 +6,7 @@
 
 namespace tut {
 
-struct test_curvebuilder_data {
+struct test_algorithm_curvebuilder_data {
     geos::io::WKTReader reader_;
 
     void checkRoundTrip(const std::string& wkt_in, const std::string& wkt_expected, double stepSizeDegrees) const {
@@ -42,10 +42,10 @@ struct test_curvebuilder_data {
     }
 };
 
-typedef test_group<test_curvebuilder_data> group;
+typedef test_group<test_algorithm_curvebuilder_data> group;
 typedef group::object object;
 
-group test_curvebuilder_group("geos::algorithm::CurveBuilder");
+group test_algorithm_curvebuilder_group("geos::algorithm::CurveBuilder");
 
 template<>
 template<>
diff --git a/tests/unit/geom/CircularStringTest.cpp b/tests/unit/geom/CircularStringTest.cpp
index 009921604..f5ab15d2d 100644
--- a/tests/unit/geom/CircularStringTest.cpp
+++ b/tests/unit/geom/CircularStringTest.cpp
@@ -509,6 +509,23 @@ void object::test<26>()
     ensure_equals_exact_geometry_xyzm(curved.get(), cs.get(), 0);
 }
 
-// TODO: Test Z, M interpolation in getLinearized()
+template<>
+template<>
+void object::test<27>()
+{
+    set_test_name("getLinearized() Z/M");
+    using XYZM = geos::geom::CoordinateXYZM;
+
+    auto cs = wktreader_.read<CircularString>("CIRCULARSTRING ZM (0 5 4 5, 5 0 6 8, 0 -5 30 40)");
+    auto ls = cs->getLinearized(30);
+
+    const CoordinateSequence* seq = ls->getCoordinatesRO();
+
+    // line point 1 is 30 degrees from the start of the arc. Z/M interpolated from CircularString points 0 and 1
+    ensure_equals_xyzm(seq->getAt<XYZM>(1), XYZM{2.5, 4.33, 4 + (6.0 - 4.0) / 3, 5 + (8.0 - 5.0) / 3}, 1e-3);
+
+    // line point 5 is 30 degrees from end of the arc. Z/M interpolated from CircularString points 1 and 2
+    ensure_equals_xyzm(seq->getAt<XYZM>(5), XYZM{2.5, -4.33, 6 + (30.0 - 6)*(2.0/3), 8 + (40.0 - 8)*(2.0/3)}, 1e-3);
+}
 
 }

commit 38ccee23700c1d572af227dc5f399f1cc45440fd
Author: Daniel Baston <dbaston at gmail.com>
Date:   Thu Jan 29 12:02:04 2026 -0500

    CurveBuilder: simplify implementation

diff --git a/include/geos/algorithm/CurveBuilder.h b/include/geos/algorithm/CurveBuilder.h
index 3f291c357..5b869a13e 100644
--- a/include/geos/algorithm/CurveBuilder.h
+++ b/include/geos/algorithm/CurveBuilder.h
@@ -37,7 +37,7 @@ public:
 
 private:
 
-    CurveBuilder(const geom::GeometryFactory& factory);
+    explicit CurveBuilder(const geom::GeometryFactory& factory);
 
     std::unique_ptr<geom::Curve> compute(const geom::LineString& ls, double distanceTolerance);
 
diff --git a/src/algorithm/CurveBuilder.cpp b/src/algorithm/CurveBuilder.cpp
index 34b2a11ed..fab9b0cb0 100644
--- a/src/algorithm/CurveBuilder.cpp
+++ b/src/algorithm/CurveBuilder.cpp
@@ -54,7 +54,7 @@ CurveBuilder::compute(const LineString& ls, double distanceTolerance) {
 
         CircularArc arc(points, start);
 
-        if (arc.isLinear()) {
+        if (arc.isLinear() || arc.getAngle() > 2*maxSpacingRadians) {
             addLineCoords(points, start, start + 2);
             start += 2;
             continue;
@@ -64,6 +64,7 @@ CurveBuilder::compute(const LineString& ls, double distanceTolerance) {
         bool foundArc = false;
 
         // Continue to consume points until we are no longer on the same arc.
+        // We need to find at least one additional point that fits on this arc to consider this an arc.
         while (stop < points.getSize()) {
             const CoordinateXY& pt = points.getAt<CoordinateXY>(stop);
 
@@ -91,34 +92,6 @@ CurveBuilder::compute(const LineString& ls, double distanceTolerance) {
             stop++;
         }
 
-        if (foundArc) {
-            double angle = Angle::angleBetweenOriented(arc.p0(), arc.getCenter(), points.getAt<CoordinateXY>(stop - 1));
-
-            if (angle == 0) {
-                angle = 2*M_PI;
-            } else if (arc.isCCW()) {
-                if (angle < 0) {
-                    // Arc is CCW but angle is CW
-                    angle = 2*M_PI + angle;
-                }
-            } else {
-                if (angle > 0) {
-                    // Arc is CW but angle is CCW
-                    angle = 2*M_PI - angle;
-                } else {
-                    angle = std::abs(angle);
-                }
-            }
-
-            constexpr int edgesPerQuadrant = 2;
-            const std::size_t minArcEdges = static_cast<std::size_t>(std::round(angle * 2 / M_PI * edgesPerQuadrant));
-            const std::size_t actualArcEdges = stop - start - 1;
-
-            if (actualArcEdges < minArcEdges) {
-                foundArc = false;
-            }
-        }
-
         if (foundArc) {
             addArc(arc, stop - 1);
             start = stop - 1;

commit b25a2ed25c74d8a3dc06c728f619ed524e611bed
Author: Daniel Baston <dbaston at gmail.com>
Date:   Tue Dec 23 11:21:46 2025 -0500

    Geometry: add getLinearized and getCurved methods

diff --git a/include/geos/algorithm/CurveBuilder.h b/include/geos/algorithm/CurveBuilder.h
new file mode 100644
index 000000000..3f291c357
--- /dev/null
+++ b/include/geos/algorithm/CurveBuilder.h
@@ -0,0 +1,65 @@
+/**********************************************************************
+*
+ * GEOS - Geometry Engine Open Source
+ * http://geos.osgeo.org
+ *
+ * Copyright (C) 2025-2026 ISciences, LLC
+ *
+ * This is free software; you can redistribute and/or modify it under
+ * the terms of the GNU Lesser General Public Licence as published
+ * by the Free Software Foundation.
+ * See the COPYING file for more information.
+ *
+ **********************************************************************/
+
+#pragma once
+
+#include <geos/export.h>
+
+#include <memory>
+
+namespace geos::geom {
+    class CircularArc;
+    class CoordinateSequence;
+    class Curve;
+    class GeometryFactory;
+    class LineString;
+    class SimpleCurve;
+}
+
+namespace geos::algorithm {
+
+class GEOS_DLL CurveBuilder {
+
+public:
+
+    static std::unique_ptr<geom::Curve> getCurved(const geom::LineString& ls, double distanceTolerance);
+
+private:
+
+    CurveBuilder(const geom::GeometryFactory& factory);
+
+    std::unique_ptr<geom::Curve> compute(const geom::LineString& ls, double distanceTolerance);
+
+    void addArc(const geom::CircularArc& arc, std::size_t stop);
+
+    void addLineCoords(const geom::CoordinateSequence& points, std::size_t from, std::size_t to);
+
+    void finishArc();
+
+    void finishLine();
+
+    std::shared_ptr<geom::CoordinateSequence> lineCoords;
+    std::shared_ptr<geom::CoordinateSequence> arcCoords;
+    std::vector<std::unique_ptr<geom::SimpleCurve>> curves;
+    const geom::GeometryFactory& factory;
+
+    /// Maximum angle between two vertices to be considered part of the same arc.
+    static constexpr double maxSpacingRadians = MATH_PI / 4 + 0.01;
+
+    /// Declared as non-copyable
+    CurveBuilder(const CurveBuilder& other);
+    CurveBuilder& operator=(const CurveBuilder& rhs);
+};
+
+}
\ No newline at end of file
diff --git a/include/geos/geom/CircularArc.h b/include/geos/geom/CircularArc.h
index 800eb0144..bef115138 100644
--- a/include/geos/geom/CircularArc.h
+++ b/include/geos/geom/CircularArc.h
@@ -117,6 +117,10 @@ public:
     /// Return the length of the arc
     double getLength() const;
 
+    /// Add linearized points representing this arc to the provided CoordinateSequence.
+    /// The origin point of the arc will NOT be added.
+    void addLinearizedPoints(CoordinateSequence& seq, double stepDegrees) const;
+
     /// Return the orientation of the arc as one of:
     /// - algorithm::Orientation::CLOCKWISE,
     /// - algorithm::Orientation::COUNTERCLOCKWISE
diff --git a/include/geos/geom/CircularString.h b/include/geos/geom/CircularString.h
index a2f0e5afb..b42af6ad8 100644
--- a/include/geos/geom/CircularString.h
+++ b/include/geos/geom/CircularString.h
@@ -15,6 +15,7 @@
 #pragma once
 
 #include <geos/geom/CircularArc.h>
+#include <geos/geom/LineString.h>
 #include <geos/geom/SimpleCurve.h>
 
 namespace geos {
@@ -55,6 +56,8 @@ public:
         return std::unique_ptr<CircularString>(reverseImpl());
     }
 
+    std::unique_ptr<Curve> getCurved(double distanceTolerance) const;
+
 protected:
 
     /// \brief
@@ -76,6 +79,12 @@ protected:
         envelope = computeEnvelopeInternal(false);
     }
 
+    CircularString* getCurvedImpl(double) const override {
+        return cloneImpl();
+    }
+
+    LineString* getLinearizedImpl(double) const override;
+
     int
     getSortIndex() const override
     {
diff --git a/include/geos/geom/CompoundCurve.h b/include/geos/geom/CompoundCurve.h
index 093e8ed57..6bedc9ce5 100644
--- a/include/geos/geom/CompoundCurve.h
+++ b/include/geos/geom/CompoundCurve.h
@@ -14,6 +14,7 @@
 
 #pragma once
 
+#include <geos/geom/LineString.h>
 #include <geos/geom/SimpleCurve.h>
 #include <geos/util.h>
 #include <vector>
@@ -117,6 +118,10 @@ protected:
         envelope = computeEnvelopeInternal();
     }
 
+    LineString* getLinearizedImpl(double) const override;
+
+    CompoundCurve* getCurvedImpl(double) const override { return cloneImpl(); }
+
     int getSortIndex() const override
     {
         return SORTINDEX_COMPOUNDCURVE;
diff --git a/include/geos/geom/Curve.h b/include/geos/geom/Curve.h
index 8f30900f2..8126f829f 100644
--- a/include/geos/geom/Curve.h
+++ b/include/geos/geom/Curve.h
@@ -16,6 +16,10 @@
 
 #include <geos/geom/Geometry.h>
 
+namespace geos::geom {
+    class LineString;
+}
+
 namespace geos {
 namespace geom {
 
@@ -71,6 +75,10 @@ public:
     /// or NULL if this is an EMPTY Curve.
     virtual std::unique_ptr<Point> getStartPoint() const = 0;
 
+    std::unique_ptr<LineString> getLinearized(double degreeSpacing) const;
+
+    std::unique_ptr<Curve> getCurved(double distanceTolerance) const;
+
     /// Returns true if the first and last coordinate in the Curve are the same
     virtual bool isClosed() const = 0;
 
@@ -88,6 +96,7 @@ public:
 protected:
     Curve(const GeometryFactory& factory) : Geometry(&factory) {}
 
+    Curve* getCurvedImpl(double distanceTolerance) const override = 0;
 };
 
 }
diff --git a/include/geos/geom/CurvePolygon.h b/include/geos/geom/CurvePolygon.h
index 1a12b4837..845db9d80 100644
--- a/include/geos/geom/CurvePolygon.h
+++ b/include/geos/geom/CurvePolygon.h
@@ -15,6 +15,7 @@
 #pragma once
 
 #include <geos/geom/SurfaceImpl.h>
+#include <geos/geom/Polygon.h>
 
 namespace geos {
 namespace geom {
@@ -35,6 +36,12 @@ public:
 
     GeometryTypeId getGeometryTypeId() const override;
 
+    std::unique_ptr<CurvePolygon> getCurved(double) {
+        return std::unique_ptr<CurvePolygon>(cloneImpl());
+    }
+
+    std::unique_ptr<Polygon> getLinearized(double degreeSpacing) const;
+
     bool hasCurvedComponents() const override;
 
     void normalize() override;
@@ -42,7 +49,11 @@ public:
 protected:
     using SurfaceImpl::SurfaceImpl;
 
-    Geometry* cloneImpl() const override;
+    CurvePolygon* cloneImpl() const override;
+
+    Polygon* getLinearizedImpl(double degreeSpacing) const override;
+
+    CurvePolygon* getCurvedImpl(double) const override { return cloneImpl(); }
 
     int
     getSortIndex() const override
diff --git a/include/geos/geom/Geometry.h b/include/geos/geom/Geometry.h
index dedf10022..ad4d0d26c 100644
--- a/include/geos/geom/Geometry.h
+++ b/include/geos/geom/Geometry.h
@@ -335,6 +335,26 @@ public:
         return this;
     }
 
+    /// Compute an approximation of the Geometry that contains no arcs.
+    /// If the Geometry already contains no arcs, a copy will be returned.
+    ///
+    /// \param degreeSpacing The spacing to use between successive points
+    ///        when approximating an arc.
+    std::unique_ptr<Geometry> getLinearized(double degreeSpacing) const
+    {
+        return std::unique_ptr<Geometry>(getLinearizedImpl(degreeSpacing));
+    }
+
+    /// Attempt to replace linear sections of a geometry with arcs, where possible.
+    /// Any arcs that already exist in the Geometry will be retained unmodified.
+    ///
+    /// \param distanceTolerance The distance tolerance to use when comparing the center
+    ///        point of arcs computed between different sets of three vertices.
+    std::unique_ptr<Geometry> getCurved(double distanceTolerance) const
+    {
+        return std::unique_ptr<Geometry>(getCurvedImpl(distanceTolerance));
+    }
+
     /**
      * \brief Tests the validity of this <code>Geometry</code>.
      *
@@ -902,6 +922,10 @@ protected:
     /// Make a geometry with coordinates in reverse order
     virtual Geometry* reverseImpl() const = 0;
 
+    virtual Geometry* getLinearizedImpl(double degreeSpacing) const = 0;
+
+    virtual Geometry* getCurvedImpl(double degreeSpacing) const = 0;
+
     /// Returns true if the array contains any non-empty Geometrys.
     template<typename T>
     static bool hasNonEmptyElements(const std::vector<T>* geometries) {
diff --git a/include/geos/geom/GeometryCollection.h b/include/geos/geom/GeometryCollection.h
index fd30f9204..a0abe1b7c 100644
--- a/include/geos/geom/GeometryCollection.h
+++ b/include/geos/geom/GeometryCollection.h
@@ -168,6 +168,14 @@ public:
     /// Returns the number of geometries in this collection
     std::size_t getNumGeometries() const override;
 
+    std::unique_ptr<GeometryCollection> getCurved(double distanceTolerance) {
+        return std::unique_ptr<GeometryCollection>(getCurvedImpl(distanceTolerance));
+    }
+
+    std::unique_ptr<GeometryCollection> getLinearized(double stepDegrees) {
+        return std::unique_ptr<GeometryCollection>(getLinearizedImpl(stepDegrees));
+    }
+
     /// Returns a pointer to the nth Geometry in this collection
     const Geometry* getGeometryN(std::size_t n) const override;
 
@@ -239,6 +247,10 @@ protected:
 
     GeometryCollection* reverseImpl() const override;
 
+    GeometryCollection* getLinearizedImpl(double) const override;
+
+    GeometryCollection* getCurvedImpl(double) const override;
+
     int
     getSortIndex() const override
     {
diff --git a/include/geos/geom/LineString.h b/include/geos/geom/LineString.h
index 25764c73d..ba3a391ee 100644
--- a/include/geos/geom/LineString.h
+++ b/include/geos/geom/LineString.h
@@ -92,6 +92,8 @@ public:
 
     double getLength() const override;
 
+    std::unique_ptr<Curve> getCurved(double distanceTolerance) const;
+
     bool isCurved() const override {
         return false;
     }
@@ -125,6 +127,10 @@ protected:
     LineString(const std::shared_ptr<const CoordinateSequence> & pts,
                const GeometryFactory& newFactory);
 
+    LineString* getLinearizedImpl(double) const override { return cloneImpl(); }
+
+    Curve* getCurvedImpl(double distanceTolerance) const override;
+
     LineString* cloneImpl() const override { return new LineString(*this); }
 
     LineString* reverseImpl() const override;
diff --git a/include/geos/geom/MultiCurve.h b/include/geos/geom/MultiCurve.h
index 5505f5d96..d4ad24222 100644
--- a/include/geos/geom/MultiCurve.h
+++ b/include/geos/geom/MultiCurve.h
@@ -50,6 +50,10 @@ public:
 
     GeometryTypeId getGeometryTypeId() const override;
 
+    std::unique_ptr<MultiLineString> getLinearized(double distanceTolerance) const {
+        return std::unique_ptr<MultiLineString>(getLinearizedImpl(distanceTolerance));
+    }
+
     bool hasDimension(Dimension::DimensionType d) const override
     {
         return d == Dimension::L;
@@ -114,6 +118,10 @@ protected:
 
     MultiCurve* reverseImpl() const override;
 
+    MultiCurve* getCurvedImpl(double) const override { return cloneImpl(); }
+
+    MultiLineString* getLinearizedImpl(double) const override;
+
     int
     getSortIndex() const override
     {
diff --git a/include/geos/geom/MultiLineString.h b/include/geos/geom/MultiLineString.h
index e8cff368a..2e4973981 100644
--- a/include/geos/geom/MultiLineString.h
+++ b/include/geos/geom/MultiLineString.h
@@ -81,6 +81,10 @@ public:
 
     GeometryTypeId getGeometryTypeId() const override;
 
+    std::unique_ptr<MultiLineString> getLinearized(double) const {
+        return clone();
+    }
+
     bool isClosed() const;
 
     std::unique_ptr<MultiLineString> clone() const
@@ -135,6 +139,10 @@ protected:
 
     MultiLineString* reverseImpl() const override;
 
+    GeometryCollection* getCurvedImpl(double disanceTolerance) const override;
+
+    MultiLineString* getLinearizedImpl(double stepDegrees) const override;
+
     int
     getSortIndex() const override
     {
diff --git a/include/geos/geom/MultiPolygon.h b/include/geos/geom/MultiPolygon.h
index f4cca3a38..991880d8d 100644
--- a/include/geos/geom/MultiPolygon.h
+++ b/include/geos/geom/MultiPolygon.h
@@ -26,7 +26,6 @@
 #include <geos/geom/GeometryCollection.h> // for inheritance
 #include <geos/geom/Polygon.h> // for inheritance
 #include <geos/geom/Dimension.h> // for Dimension::DimensionType
-#include <geos/geom/GeometryCollection.h>
 
 
 // Forward declarations
@@ -132,6 +131,10 @@ protected:
 
     MultiPolygon* cloneImpl() const override { return new MultiPolygon(*this); }
 
+    GeometryCollection* getCurvedImpl(double distanceTolerance) const override;
+
+    MultiPolygon* getLinearizedImpl(double) const override { return cloneImpl(); }
+
     MultiPolygon* reverseImpl() const override;
 
     int
diff --git a/include/geos/geom/MultiSurface.h b/include/geos/geom/MultiSurface.h
index 73871e4f0..ed9ab49f5 100644
--- a/include/geos/geom/MultiSurface.h
+++ b/include/geos/geom/MultiSurface.h
@@ -49,6 +49,10 @@ public:
 
     GeometryTypeId getGeometryTypeId() const override;
 
+    std::unique_ptr<MultiPolygon> getLinearized(double degreeSpacing) const {
+        return std::unique_ptr<MultiPolygon>(getLinearizedImpl(degreeSpacing));
+    }
+
     bool hasDimension(Dimension::DimensionType d) const override
     {
         return d == Dimension::A;
@@ -81,6 +85,10 @@ protected:
         return new MultiSurface(*this);
     }
 
+    MultiSurface* getCurvedImpl(double) const override { return cloneImpl(); }
+
+    MultiPolygon* getLinearizedImpl(double) const override;
+
     int
     getSortIndex() const override
     {
diff --git a/include/geos/geom/Point.h b/include/geos/geom/Point.h
index cc944d972..79fd3af96 100644
--- a/include/geos/geom/Point.h
+++ b/include/geos/geom/Point.h
@@ -187,6 +187,10 @@ protected:
 
     Point(const Point& p);
 
+    Point* getCurvedImpl(double) const override { return cloneImpl(); };
+
+    Point* getLinearizedImpl(double) const override { return cloneImpl(); };
+
     Point* cloneImpl() const override { return new Point(*this); }
 
     Point* reverseImpl() const override { return new Point(*this); }
diff --git a/include/geos/geom/Polygon.h b/include/geos/geom/Polygon.h
index 98e4fe262..b4499e514 100644
--- a/include/geos/geom/Polygon.h
+++ b/include/geos/geom/Polygon.h
@@ -94,6 +94,12 @@ public:
     std::string getGeometryType() const override;
     GeometryTypeId getGeometryTypeId() const override;
 
+    std::unique_ptr<Surface> getCurved(double distanceTolerance) const {
+        return std::unique_ptr<Surface>(getCurvedImpl(distanceTolerance));
+    }
+
+    std::unique_ptr<Polygon> getLinearized(double) const;
+
     void normalize() override;
 
     std::unique_ptr<Polygon> reverse() const { return std::unique_ptr<Polygon>(reverseImpl()); }
@@ -116,6 +122,10 @@ protected:
 
     using SurfaceImpl::SurfaceImpl;
 
+    Surface* getCurvedImpl(double) const override;
+
+    Polygon* getLinearizedImpl(double) const override { return cloneImpl(); };
+
     Polygon* cloneImpl() const override { return new Polygon(*this); }
 
     Polygon* reverseImpl() const override;
diff --git a/src/algorithm/CurveBuilder.cpp b/src/algorithm/CurveBuilder.cpp
new file mode 100644
index 000000000..34b2a11ed
--- /dev/null
+++ b/src/algorithm/CurveBuilder.cpp
@@ -0,0 +1,214 @@
+/**********************************************************************
+*
+ * GEOS - Geometry Engine Open Source
+ * http://geos.osgeo.org
+ *
+ * Copyright (C) 2025-2026 ISciences, LLC
+ *
+ * This is free software; you can redistribute and/or modify it under
+ * the terms of the GNU Lesser General Public Licence as published
+ * by the Free Software Foundation.
+ * See the COPYING file for more information.
+ *
+ **********************************************************************/
+
+#include <geos/algorithm/Angle.h>
+#include <geos/algorithm/CurveBuilder.h>
+
+#include <geos/geom/CircularArc.h>
+#include <geos/geom/CircularString.h>
+#include <geos/geom/CompoundCurve.h>
+#include <geos/geom/GeometryFactory.h>
+#include <geos/geom/LineString.h>
+
+namespace geos::algorithm {
+
+using geom::CircularArc;
+using geom::CoordinateSequence;
+using geom::CoordinateXY;
+using geom::CoordinateXYZM;
+using geom::GeometryFactory;
+using geom::LineString;
+
+CurveBuilder::CurveBuilder(const GeometryFactory &p_factory)
+    : factory(p_factory) {}
+
+std::unique_ptr<geom::Curve>
+CurveBuilder::getCurved(const LineString &ls, double distanceTolerance) {
+    CurveBuilder cb(*ls.getFactory());
+    return cb.compute(ls, distanceTolerance);
+}
+
+std::unique_ptr<geom::Curve>
+CurveBuilder::compute(const LineString& ls, double distanceTolerance) {
+    const auto& points = *ls.getSharedCoordinates();
+
+    std::size_t start = 0;
+
+    while (start + 1 < points.getSize()) {
+
+        if (start + 2 >= points.getSize()) {
+            addLineCoords(points, start, start + 1);
+            break;
+        }
+
+        CircularArc arc(points, start);
+
+        if (arc.isLinear()) {
+            addLineCoords(points, start, start + 2);
+            start += 2;
+            continue;
+        }
+
+        std::size_t stop = start + 3;
+        bool foundArc = false;
+
+        // Continue to consume points until we are no longer on the same arc.
+        while (stop < points.getSize()) {
+            const CoordinateXY& pt = points.getAt<CoordinateXY>(stop);
+
+            const double distance = pt.distance(arc.getCenter());
+
+            // Does the radius match?
+            if (std::abs(distance - arc.getRadius()) > distanceTolerance) {
+                break;
+            }
+
+            const CoordinateXY& prev1 = points.getAt<CoordinateXY>(stop - 1);
+            const CoordinateXY& prev2 = points.getAt<CoordinateXY>(stop - 2);
+
+            // Is the angle similar to, or less than, the previous ones?
+            const double prevAngle = Angle::angleBetween(arc.p0(), arc.p1(), arc.p2());
+            const double currAngle = Angle::angleBetween(prev2, prev1, pt);
+
+            double angleTolerance = prevAngle*1e-3;
+
+            if (std::abs(currAngle - prevAngle) > angleTolerance) {
+                break;
+            }
+
+            foundArc = true;
+            stop++;
+        }
+
+        if (foundArc) {
+            double angle = Angle::angleBetweenOriented(arc.p0(), arc.getCenter(), points.getAt<CoordinateXY>(stop - 1));
+
+            if (angle == 0) {
+                angle = 2*M_PI;
+            } else if (arc.isCCW()) {
+                if (angle < 0) {
+                    // Arc is CCW but angle is CW
+                    angle = 2*M_PI + angle;
+                }
+            } else {
+                if (angle > 0) {
+                    // Arc is CW but angle is CCW
+                    angle = 2*M_PI - angle;
+                } else {
+                    angle = std::abs(angle);
+                }
+            }
+
+            constexpr int edgesPerQuadrant = 2;
+            const std::size_t minArcEdges = static_cast<std::size_t>(std::round(angle * 2 / M_PI * edgesPerQuadrant));
+            const std::size_t actualArcEdges = stop - start - 1;
+
+            if (actualArcEdges < minArcEdges) {
+                foundArc = false;
+            }
+        }
+
+        if (foundArc) {
+            addArc(arc, stop - 1);
+            start = stop - 1;
+        } else {
+            addLineCoords(points, start, start + 1);
+            start++;
+        }
+
+    }
+
+    finishArc();
+    finishLine();
+
+    if (curves.size() == 1) {
+        return std::move(curves.front());
+    }
+
+    return factory.createCompoundCurve(std::move(curves));
+}
+
+void
+CurveBuilder::addLineCoords(const CoordinateSequence& points, std::size_t from, std::size_t to)
+{
+    finishArc();
+
+    if (lineCoords) {
+        lineCoords->add(points, from + 1, to);
+    } else {
+        lineCoords = std::make_shared<CoordinateSequence>(0, points.hasZ(), points.hasM());
+        lineCoords->add(points, from, to);
+    }
+}
+
+void
+CurveBuilder::finishArc()
+{
+    if (arcCoords) {
+        curves.push_back(factory.createCircularString(arcCoords));
+        arcCoords.reset();
+    }
+}
+
+void
+CurveBuilder::finishLine() {
+    if (lineCoords) {
+        curves.push_back(factory.createLineString(lineCoords));
+        lineCoords.reset();
+    }
+}
+
+void
+CurveBuilder::addArc(const CircularArc& arc, std::size_t stop) {
+    finishLine();
+
+    const CoordinateSequence& points = *arc.getCoordinateSequence();
+    std::size_t start = arc.getCoordinatePosition();
+
+    double xSum = 0.0;
+    double ySum = 0.0;
+    std::size_t nArcApproximations = 0;
+
+    for (std::size_t i = start; i <= stop - 2; i++) {
+        CircularArc a(points, i);
+        const CoordinateXY& center = a.getCenter();
+
+        xSum += center.x;
+        ySum += center.y;
+        nArcApproximations++;
+    }
+
+    const CoordinateXY averageCenter = { xSum / static_cast<double>(nArcApproximations), ySum / static_cast<double>(nArcApproximations) };
+
+    CoordinateXYZM p0, p1, p2;
+    points.getAt(start, p0);
+    points.getAt(stop, p2);
+
+    const double averageRadius = 0.5*(p0.distance(averageCenter) + p2.distance(averageCenter));
+
+    p1 = CircularArcs::getMidpoint(p0, p2, averageCenter, averageRadius, arc.isCCW());
+    p1.z = 0.5*(p0.z + p2.z);
+    p1.m = 0.5*(p0.m + p2.m);
+
+    if (!arcCoords) {
+        arcCoords = std::make_shared<CoordinateSequence>( points.hasZ(), points.hasM());
+        arcCoords->reserve(3);
+        arcCoords->add(p0);
+    }
+
+    arcCoords->add(p1);
+    arcCoords->add(p2);
+}
+
+}
\ No newline at end of file
diff --git a/src/geom/CircularArc.cpp b/src/geom/CircularArc.cpp
index ed622c135..561ab4671 100644
--- a/src/geom/CircularArc.cpp
+++ b/src/geom/CircularArc.cpp
@@ -345,6 +345,57 @@ CircularArc::getLength() const
     return getAngle()*getRadius();
 }
 
+
+void
+CircularArc::addLinearizedPoints(CoordinateSequence& seq, double stepDegrees) const
+{
+    if (isLinear()) {
+        seq.add(*getCoordinateSequence(), getCoordinatePosition() + 1, getCoordinatePosition() + 2);
+        return;
+    }
+
+    const double angle = getAngle();
+    const bool isCCW = getOrientation() == geos::algorithm::Orientation::COUNTERCLOCKWISE;
+    const double stepRad = stepDegrees * MATH_PI / 180.0;
+    const int nSegments = std::max(static_cast<int>(std::ceil(angle / stepRad)), 2);
+
+    double adjStepRad = angle / nSegments;
+
+    const CoordinateXY& center = getCenter();
+    const double radius = getRadius();
+
+    // To ensure that the vertices in the linearized arc are independent of the
+    // arc orientation, we process the arc in a CCW manner regardless of its
+    // original orientation.
+    const double startAngle = isCCW ? theta0() : theta2();
+
+    const bool hasZ = getCoordinateSequence()->hasZ();
+    const bool hasM = getCoordinateSequence()->hasM();
+
+    CoordinateXYZM p0, p2;
+    getCoordinateSequence()->getAt(getCoordinatePosition(), p0);
+    getCoordinateSequence()->getAt(getCoordinatePosition() + 2, p2);
+
+    for (int i = 1; i < nSegments; i++) {
+        const int j = isCCW ? i: nSegments - i;
+        const double frac = static_cast<double>(j) / nSegments;
+        const double theta = startAngle + j*adjStepRad;
+
+        CoordinateXYZM pt{geos::algorithm::CircularArcs::createPoint(center, radius, theta)};
+
+        if (hasZ) {
+            pt.z = p0.z + frac*(p2.z - p0.z);
+        }
+        if (hasM) {
+            pt.m = p0.m + frac*(p2.m - p0.m);
+        }
+
+        seq.add(pt);
+    }
+
+    seq.add(p2);
+}
+
 bool
 CircularArc::isUpwardAtPoint(const CoordinateXY& q) const
 {
@@ -463,4 +514,5 @@ CircularArc::toString() const {
     return ss.str();
 }
 
+
 }
diff --git a/src/geom/CircularString.cpp b/src/geom/CircularString.cpp
index fe6b79cd5..83d4ceeda 100644
--- a/src/geom/CircularString.cpp
+++ b/src/geom/CircularString.cpp
@@ -116,8 +116,7 @@ CircularString::normalize()
 
 /*private*/
 void
-CircularString::normalizeClosed()
-{
+CircularString::normalizeClosed() {
     if (isEmpty()) {
         return;
     }
@@ -154,6 +153,30 @@ CircularString::normalizeClosed()
     }
 }
 
+std::unique_ptr<Curve>
+CircularString::getCurved(double) const
+{
+    return getFactory()->createCircularString(points);
+}
+
+
+LineString*
+CircularString::getLinearizedImpl(double degreeSpacing) const {
+    if (isEmpty()) {
+        return getFactory()->createLineString().release();
+    }
+
+    auto seq = std::make_shared<CoordinateSequence>(0, hasZ(), hasM());
+    seq->add(*getCoordinatesRO(), static_cast<std::size_t>(0), 0);
+
+    for (const CircularArc& arc : getArcs())
+    {
+        arc.addLinearizedPoints(*seq, degreeSpacing);
+    }
+
+    return getFactory()->createLineString(seq).release();
+}
+
 CircularString*
 CircularString::reverseImpl() const
 {
diff --git a/src/geom/CompoundCurve.cpp b/src/geom/CompoundCurve.cpp
index bea7c5a6d..9d771474e 100644
--- a/src/geom/CompoundCurve.cpp
+++ b/src/geom/CompoundCurve.cpp
@@ -15,6 +15,7 @@
 #include <sstream>
 
 #include <geos/algorithm/Orientation.h>
+#include <geos/geom/CircularArc.h>
 #include <geos/geom/CompoundCurve.h>
 #include <geos/geom/CoordinateFilter.h>
 #include <geos/geom/CircularString.h>
@@ -23,6 +24,7 @@
 #include <geos/operation/split/SplitLinealAtPoint.h>
 #include <geos/util.h>
 
+
 namespace geos {
 namespace geom {
 
@@ -251,6 +253,30 @@ CompoundCurve::getLength() const
     return sum;
 }
 
+LineString*
+CompoundCurve::getLinearizedImpl(double degreeSpacing) const
+{
+    auto seq = std::make_shared<CoordinateSequence>(0, hasZ(), hasM());
+    for (const auto& curve : curves) {
+        const CoordinateSequence* curveSeq = curve->getCoordinatesRO();
+
+        if (seq->isEmpty()) {
+            seq->add(*curveSeq, static_cast<size_t>(0), 0);
+        }
+
+        if (curve->isCurved()) {
+            for (std::size_t i = 0; i < curveSeq->size() - 2; i += 2)
+            {
+                CircularArc arc(*curveSeq, i);
+                arc.addLinearizedPoints(*seq, degreeSpacing);
+            }
+        } else {
+            seq->add(*curveSeq, 1, curveSeq->size() - 1);
+        }
+    }
+    return getFactory()->createLineString(std::move(seq)).release();
+}
+
 std::size_t
 CompoundCurve::getNumCurves() const
 {
diff --git a/src/geom/Curve.cpp b/src/geom/Curve.cpp
index 26b7dc533..7a748f12b 100644
--- a/src/geom/Curve.cpp
+++ b/src/geom/Curve.cpp
@@ -16,6 +16,8 @@
 
 #include <geos/geom/Curve.h>
 #include <geos/geom/GeometryFilter.h>
+#include <geos/geom/LineString.h>
+#include <geos/util.h>
 
 namespace geos {
 namespace geom {
@@ -61,11 +63,22 @@ Curve::isRing() const
 }
 
 std::unique_ptr<Curve>
-Curve::reverse() const
-{
+Curve::reverse() const {
     return std::unique_ptr<Curve>(static_cast<Curve*>(Geometry::reverse().release()));
 }
 
+std::unique_ptr<LineString>
+Curve::getLinearized(double degreeSpacing) const
+{
+    return std::unique_ptr<LineString>(detail::down_cast<LineString*>(getLinearizedImpl(degreeSpacing)));
+}
+
+std::unique_ptr<Curve>
+Curve::getCurved(double distanceTolerance) const
+{
+    return std::unique_ptr<Curve>(getCurvedImpl(distanceTolerance));
+}
+
 
 }
 }
diff --git a/src/geom/CurvePolygon.cpp b/src/geom/CurvePolygon.cpp
index 7f07e9f41..a9e34f369 100644
--- a/src/geom/CurvePolygon.cpp
+++ b/src/geom/CurvePolygon.cpp
@@ -101,6 +101,28 @@ namespace geom {
         return sum;
     }
 
+    std::unique_ptr<Polygon>
+    CurvePolygon::getLinearized(double spacingDegrees) const {
+        return std::unique_ptr<Polygon>(getLinearizedImpl(spacingDegrees));
+    }
+
+    Polygon*
+    CurvePolygon::getLinearizedImpl(double spacingDegrees) const {
+        const auto& gfact = *getFactory();
+
+        auto linShell = gfact.createLinearRing(shell->getLinearized(spacingDegrees)->getSharedCoordinates());
+
+        if (holes.empty()) {
+            return getFactory()->createPolygon(std::move(linShell)).release();
+        }
+
+        std::vector<std::unique_ptr<LinearRing>> linHoles(holes.size());
+        for (size_t i = 0; i < holes.size(); i++) {
+            linHoles[i] = gfact.createLinearRing(holes[i]->getLinearized(spacingDegrees)->getSharedCoordinates());
+        }
+        return getFactory()->createPolygon(std::move(linShell), std::move(linHoles)).release();
+    }
+
     bool CurvePolygon::hasCurvedComponents() const {
         if (shell->hasCurvedComponents()) {
             return true;
@@ -113,7 +135,7 @@ namespace geom {
         return false;
     }
 
-    Geometry*
+    CurvePolygon*
     CurvePolygon::cloneImpl() const {
         return new CurvePolygon(*this);
     }
diff --git a/src/geom/GeometryCollection.cpp b/src/geom/GeometryCollection.cpp
index 25877b81a..9b61fbf86 100644
--- a/src/geom/GeometryCollection.cpp
+++ b/src/geom/GeometryCollection.cpp
@@ -107,6 +107,30 @@ GeometryCollection::getCoordinates() const
     return coordinates;
 }
 
+GeometryCollection*
+GeometryCollection::getCurvedImpl(double distanceTolerance) const
+{
+    std::vector<std::unique_ptr<Geometry>> curvedGeoms(geometries.size());
+    for (std::size_t i = 0; i < geometries.size(); i++) {
+        curvedGeoms[i] = geometries[i]->getCurved(distanceTolerance);
+    }
+
+    return getFactory()->createGeometryCollection(std::move(curvedGeoms)).release();
+}
+
+
+GeometryCollection*
+GeometryCollection::getLinearizedImpl(double stepDegrees) const
+{
+    std::vector<std::unique_ptr<Geometry>> linGeoms(geometries.size());
+    for (std::size_t i = 0; i < geometries.size(); i++) {
+        linGeoms[i] = geometries[i]->getLinearized(stepDegrees);
+    }
+
+    return getFactory()->createGeometryCollection(std::move(linGeoms)).release();
+}
+
+
 bool
 GeometryCollection::isEmpty() const
 {
diff --git a/src/geom/LineString.cpp b/src/geom/LineString.cpp
index c05932754..e1eeb2f1d 100644
--- a/src/geom/LineString.cpp
+++ b/src/geom/LineString.cpp
@@ -19,8 +19,13 @@
  **********************************************************************/
 
 #include <geos/util/IllegalArgumentException.h>
+#include <geos/algorithm/CircularArcs.h>
+#include <geos/algorithm/CurveBuilder.h>
 #include <geos/algorithm/Length.h>
 #include <geos/algorithm/Orientation.h>
+#include <geos/geom/CircularArc.h>
+#include <geos/geom/CircularString.h>
+#include <geos/geom/CompoundCurve.h>
 #include <geos/geom/Coordinate.h>
 #include <geos/geom/CoordinateSequence.h>
 #include <geos/geom/CoordinateSequenceFilter.h>
@@ -101,6 +106,17 @@ LineString::validateConstruction()
     }
 }
 
+Curve*
+LineString::getCurvedImpl(double distanceTolerance) const
+{
+    return getCurved(distanceTolerance).release();
+}
+
+std::unique_ptr<Curve>
+LineString::getCurved(double distanceTolerance) const
+{
+    return CurveBuilder::getCurved(*this, distanceTolerance);
+}
 
 std::string
 LineString::getGeometryType() const
diff --git a/src/geom/MultiCurve.cpp b/src/geom/MultiCurve.cpp
index 31e13eb2c..ccdc7725f 100644
--- a/src/geom/MultiCurve.cpp
+++ b/src/geom/MultiCurve.cpp
@@ -78,6 +78,18 @@ MultiCurve::getGeometryTypeId() const
     return GEOS_MULTICURVE;
 }
 
+MultiLineString*
+MultiCurve::getLinearizedImpl(double stepSizeDegrees) const
+{
+    std::vector<std::unique_ptr<Geometry>> lines(geometries.size());
+
+    for (std::size_t i = 0; i < geometries.size(); i++) {
+        lines[i] = geometries[i]->getLinearized(stepSizeDegrees);
+    }
+
+    return getFactory()->createMultiLineString(std::move(lines)).release();
+}
+
 bool
 MultiCurve::isClosed() const
 {
diff --git a/src/geom/MultiLineString.cpp b/src/geom/MultiLineString.cpp
index 3a2b1ac37..be9f15b06 100644
--- a/src/geom/MultiLineString.cpp
+++ b/src/geom/MultiLineString.cpp
@@ -20,12 +20,12 @@
 
 #include <geos/geomgraph/GeometryGraph.h>
 #include <geos/geom/GeometryFactory.h>
+#include <geos/geom/MultiCurve.h>
 #include <geos/geom/MultiLineString.h>
 #include <geos/operation/BoundaryOp.h>
 #include <geos/util.h>
 
 #include <vector>
-#include <cassert>
 
 using geos::geom::GeometryFactory;
 using geos::geom::MultiLineString;
@@ -106,6 +106,31 @@ MultiLineString::getGeometryTypeId() const
     return GEOS_MULTILINESTRING;
 }
 
+GeometryCollection*
+MultiLineString::getCurvedImpl(double disanceTolerance) const
+{
+    std::vector<std::unique_ptr<Geometry>> curvedGeoms(geometries.size());
+
+    bool hasCurves = false;
+
+    for (std::size_t i = 0; i < geometries.size(); i++) {
+        curvedGeoms[i] = detail::down_cast<Curve*>(geometries[i].get())->getCurved(disanceTolerance);
+        hasCurves |= curvedGeoms[i]->hasCurvedComponents();
+    }
+
+    if (hasCurves) {
+        return getFactory()->createMultiCurve(std::move(curvedGeoms)).release();
+    }
+
+    return getFactory()->createMultiLineString(std::move(curvedGeoms)).release();
+}
+
+MultiLineString*
+MultiLineString::getLinearizedImpl(double) const
+{
+    return cloneImpl();
+}
+
 MultiLineString*
 MultiLineString::reverseImpl() const
 {
diff --git a/src/geom/MultiPolygon.cpp b/src/geom/MultiPolygon.cpp
index fa7cbaf71..a4282df2d 100644
--- a/src/geom/MultiPolygon.cpp
+++ b/src/geom/MultiPolygon.cpp
@@ -22,6 +22,7 @@
 #include <geos/geom/Polygon.h>
 #include <geos/geom/MultiPolygon.h>
 #include <geos/geom/MultiLineString.h>
+#include <geos/geom/MultiSurface.h>
 #include <geos/geom/GeometryFactory.h>
 #include <geos/geom/Dimension.h>
 
@@ -87,6 +88,26 @@ MultiPolygon::getBoundary() const
     return getFactory()->createMultiLineString(std::move(allRings));
 }
 
+GeometryCollection*
+MultiPolygon::getCurvedImpl(double distanceTolerance) const {
+    std::vector<std::unique_ptr<Geometry>> curvedGeoms(geometries.size());
+
+    bool hasCurves = false;
+
+    for (std::size_t i = 0; i < geometries.size(); i++) {
+        curvedGeoms[i] = geometries[i]->getCurved(distanceTolerance);
+        if (curvedGeoms[i]->hasCurvedComponents()) {
+            hasCurves = true;
+        }
+    }
+
+    if (hasCurves) {
+        return getFactory()->createMultiSurface(std::move(curvedGeoms)).release();
+    }
+
+    return getFactory()->createMultiPolygon(std::move(curvedGeoms)).release();
+}
+
 GeometryTypeId
 MultiPolygon::getGeometryTypeId() const
 {
diff --git a/src/geom/MultiSurface.cpp b/src/geom/MultiSurface.cpp
index b8e299f6a..f390e9958 100644
--- a/src/geom/MultiSurface.cpp
+++ b/src/geom/MultiSurface.cpp
@@ -86,6 +86,18 @@ MultiSurface::getGeometryTypeId() const
     return GEOS_MULTISURFACE;
 }
 
+MultiPolygon*
+MultiSurface::getLinearizedImpl(double stepSizeDegrees) const
+{
+    std::vector<std::unique_ptr<Geometry>> polygons(geometries.size());
+
+    for (std::size_t i = 0; i < geometries.size(); i++) {
+        polygons[i] = geometries[i]->getLinearized(stepSizeDegrees);
+    }
+
+    return getFactory()->createMultiPolygon(std::move(polygons)).release();
+}
+
 MultiSurface*
 MultiSurface::reverseImpl() const
 {
diff --git a/src/geom/Polygon.cpp b/src/geom/Polygon.cpp
index 116374ca9..b4e4864eb 100644
--- a/src/geom/Polygon.cpp
+++ b/src/geom/Polygon.cpp
@@ -22,6 +22,7 @@
 #include <geos/algorithm/Orientation.h>
 #include <geos/util.h>
 #include <geos/geom/Coordinate.h>
+#include <geos/geom/CurvePolygon.h>
 #include <geos/geom/Polygon.h>
 #include <geos/geom/LinearRing.h>
 #include <geos/geom/MultiLineString.h> // for getBoundary()
@@ -66,6 +67,39 @@ Polygon::getCoordinates() const
     return cl;
 }
 
+Surface*
+Polygon::getCurvedImpl(double distanceTolerance) const
+{
+    auto curvedShell = shell->getCurved(distanceTolerance);
+    bool isCurved = curvedShell->hasCurvedComponents();
+
+    if (holes.empty()) {
+        if (isCurved) {
+            return getFactory()->createCurvePolygon(std::move(curvedShell)).release();
+        } else {
+            return cloneImpl();
+        }
+    }
+
+    std::vector<std::unique_ptr<Curve>> holesCurved(holes.size());
+    for (std::size_t i = 0; i < holes.size(); i++) {
+        holesCurved[i] = holes[i]->getCurved(distanceTolerance);
+        isCurved |= holesCurved[i]->hasCurvedComponents();
+    }
+
+    if (isCurved) {
+        return getFactory()->createCurvePolygon(std::move(curvedShell), std::move(holesCurved)).release();
+    }
+
+    return cloneImpl();
+}
+
+std::unique_ptr<Polygon>
+Polygon::getLinearized(double degreeSpacing) const
+{
+    return std::unique_ptr<Polygon>(getLinearizedImpl(degreeSpacing));
+}
+
 std::string
 Polygon::getGeometryType() const
 {
diff --git a/tests/unit/algorithm/CurveBuilderTest.cpp b/tests/unit/algorithm/CurveBuilderTest.cpp
new file mode 100644
index 000000000..10bdd6f7d
--- /dev/null
+++ b/tests/unit/algorithm/CurveBuilderTest.cpp
@@ -0,0 +1,223 @@
+#include <tut/tut.hpp>
+
+#include <geos/io/WKTReader.h>
+
+#include "utility.h"
+
+namespace tut {
+
+struct test_curvebuilder_data {
+    geos::io::WKTReader reader_;
+
+    void checkRoundTrip(const std::string& wkt_in, const std::string& wkt_expected, double stepSizeDegrees) const {
+        auto inCurve = reader_.read(wkt_in);
+
+        auto linearized = inCurve->getLinearized(stepSizeDegrees);
+        auto outCurve = linearized->getCurved(1e-6);
+        auto expected = reader_.read(wkt_expected);
+
+        double distanceTolerance = 1e-4;
+
+        ensure_equals_exact_geometry_xyzm(outCurve.get(), expected.get(), distanceTolerance);
+    }
+
+    void checkRoundTripUnchanged(const std::string& wkt_in, double stepSizeDegrees) const {
+        checkRoundTrip(wkt_in, wkt_in, stepSizeDegrees);
+    }
+
+    void checkLineToCurve(const std::string& wkt_in, const std::string& wkt_expected, double distanceTolerance) const {
+        auto ls = reader_.read<LineString>(wkt_in);
+        auto curve = ls->getCurved(distanceTolerance);
+        auto expected = reader_.read(wkt_expected);
+
+        ensure_equals_exact_geometry_xyzm(curve.get(), expected.get(), distanceTolerance);
+    }
+
+    void checkLineToCurveUnchanged(const std::string& wkt_in, double distanceTolerance) const {
+        auto ls = reader_.read<LineString>(wkt_in);
+        auto expected = ls->clone();
+        auto curve = ls->getCurved(distanceTolerance);
+
+        ensure_equals_exact_geometry_xyzm(curve.get(), expected.get(), 0.0);
+    }
+};
+
+typedef test_group<test_curvebuilder_data> group;
+typedef group::object object;
+
+group test_curvebuilder_group("geos::algorithm::CurveBuilder");
+
+template<>
+template<>
+void object::test<1>()
+{
+    set_test_name("two-point LineString");
+
+    checkLineToCurveUnchanged("LINESTRING (3 4, 2 7)", 1);
+}
+
+template<>
+template<>
+void object::test<2>()
+{
+    set_test_name("LineString of three collinear points");
+
+    checkLineToCurveUnchanged("LINESTRING (0 0, 1 2, 2 4)", 1);
+}
+
+template<>
+template<>
+void object::test<3>()
+{
+    set_test_name("coarsely linearized semicircle");
+
+    checkLineToCurve("LINESTRING(0 0,29.2893 70.7107,100 100,170.7107 70.7107,200 0)",
+                     "CIRCULARSTRING (0 0, 100 100, 200 0)",
+                     2e-3);
+}
+
+template<>
+template<>
+void object::test<4>()
+{
+    set_test_name("coarsely linearized closed semicircle");
+
+    checkLineToCurve("LINESTRING(0 0,29.2893 70.7107,100 100,170.7107 70.7107,200 0, 0 0)",
+                     "COMPOUNDCURVE (CIRCULARSTRING (0 0, 100 100, 200 0), LINESTRING (200 0, 0 0))",
+                     2.5e-3);
+}
+
+template<>
+template<>
+void object::test<5>()
+{
+    set_test_name("round-trip with 3-quadrant CircularString");
+
+    // cu_lwstroke.c: 392
+    checkRoundTrip("CIRCULARSTRING (-1 0, 0 1, 0 -1)",
+                   "CIRCULARSTRING (-1 0, 0.70710678 0.707010678, 0 -1)",
+                   90.0 / 8);
+}
+
+template<>
+template<>
+void object::test<6>()
+{
+    set_test_name("Two-part CompoundCurve round-trip");
+
+    // cu_lwstroke.c: 404
+    checkRoundTrip("COMPOUNDCURVE (CIRCULARSTRING (-1 0, 0 1, 0 -1), (0 -1, -1 -1))",
+                   "COMPOUNDCURVE( CIRCULARSTRING (-1 0, 0.70710678 0.707010678, 0 -1), (0 -1, -1 -1))",
+                   90.0 / 8);
+}
+
+template<>
+template<>
+void object::test<7>()
+{
+    set_test_name("Three-part CompoundCurve round-trip");
+
+    // cu_lwstroke.c: 416
+    checkRoundTrip("COMPOUNDCURVE((-3 -3,-1 0),CIRCULARSTRING(-1 0,0 1,0 -1),(0 -1,0 -1.5,0 -2),CIRCULARSTRING(0 -2,-1 -3,1 -3),(1 -3,5 5))",
+                   "COMPOUNDCURVE((-3 -3,-1 0),CIRCULARSTRING(-1 0,0.70710678 0.70710678,0 -1),(0 -1,0 -1.5,0 -2),CIRCULARSTRING(0 -2,-0.70710678 -3.70710678,1 -3),(1 -3,5 5))",
+                   90.0 / 8);
+}
+
+template<>
+template<>
+void object::test<8>()
+{
+    set_test_name("CompoundCurve with two CircularStrings, round-trip");
+
+    // cu_lwstroke.c: 432
+    // NOTE: The expected result here is modified from the original. It is not clear why liblwgeom returns a
+    // CompoundCurve with two CircularStrings instead of a single CircularString (like GDAL). The expected result
+    // used here is from GDAL.
+    checkRoundTrip("COMPOUNDCURVE(CIRCULARSTRING(-1 0,0 1,0 -1),CIRCULARSTRING(0 -1,-1 -2,1 -2))",
+                   "CIRCULARSTRING(-1 0,0.70710678 0.70710678,0 -1,-0.70710678 -2.70710678,1 -2)",
+                   90.0 / 8);
+}
+
+template<>
+template<>
+void object::test<9>()
+{
+    set_test_name("CompoundCurve with a CircularString between two LineStrings, round-trip");
+
+    // cu_lwstroke.c: 447
+    checkRoundTripUnchanged("COMPOUNDCURVE((0 0, 1 1), CIRCULARSTRING(1 1, 2 2, 3 1), (3 1, 4 4))",
+                       90.0 / 8);
+}
+
+template<>
+template<>
+void object::test<10>()
+{
+    set_test_name("LineString forming a square, round-trip");
+
+    // cu_lwstroke.c: 461
+    checkRoundTripUnchanged("LINESTRING(0 0,10 0,10 10,0 10,0 0)",
+                       90.0 / 8);
+
+    // cu_lwstroke.c: 469
+    checkRoundTripUnchanged("LINESTRING(10 10,0 10,0 0,10 0)",
+                            90.0 / 8);
+
+    // cu_lwstroke.c: 478
+    checkRoundTripUnchanged("LINESTRING(0 0,10 0,10 10,0 10)",
+                       90.0 / 8);
+}
+
+template<>
+template<>
+void object::test<11>()
+{
+    set_test_name("collection of two-point LineStrings, round-trip");
+
+    // cu_lwstroke.c: 497
+    checkRoundTripUnchanged("GEOMETRYCOLLECTION(LINESTRING(10 10,10 11),LINESTRING(10 11,11 11),LINESTRING(11 11,10 10))",
+                       90.0 / 8);
+}
+
+template<>
+template<>
+void object::test<13>()
+{
+    set_test_name("collection of two-point LineStrings and a CircularString, round-trip");
+
+    // cu_lwstroke.c: 508
+    checkRoundTripUnchanged("GEOMETRYCOLLECTION(LINESTRING(4 4,4 8),CIRCULARSTRING(4 8,6 10,8 8),LINESTRING(8 8,8 4))",
+                       90.0 / 8);
+}
+
+template<>
+template<>
+void object::test<14>()
+{
+    set_test_name("round-trip with 5-point CircularString");
+
+    checkRoundTripUnchanged("CIRCULARSTRING (-5 0, 0 5, 5 0, 4 1, 3 0)",
+                   90.0 / 4);
+}
+
+template<>
+template<>
+void object::test<15>()
+{
+    set_test_name("constructed curve does not depend on direction of input");
+
+    auto cs = reader_.read("CIRCULARSTRING (-5 0, 0 5, 5 0)");
+
+    auto lin = cs->getLinearized(90.0 / 4);
+    auto linRev = lin->reverse();
+
+    auto curveRev = linRev->getCurved(1e-4);
+    auto curve1 = curveRev->reverse();
+
+    auto curve2 = lin->getCurved(1e-4);
+
+    ensure_equals_exact_geometry_xyzm(curve1.get(), curve2.get(), 0);
+}
+
+
+}
diff --git a/tests/unit/geom/CircularStringTest.cpp b/tests/unit/geom/CircularStringTest.cpp
index 3678746c9..009921604 100644
--- a/tests/unit/geom/CircularStringTest.cpp
+++ b/tests/unit/geom/CircularStringTest.cpp
@@ -9,6 +9,7 @@
 #include <geos/util/UnsupportedOperationException.h>
 
 #include "utility.h"
+#include "geos/geom/CircularArc.h"
 
 using geos::geom::CoordinateSequence;
 using geos::geom::CircularString;
@@ -44,6 +45,36 @@ struct test_circularstring_data {
 
         ensure_equals_exact_geometry_xyzm(cs.get(), expected.get(), 0);
     }
+
+    void checkLinearizeMaxDeviation(const std::string& wkt_in, const std::string& wkt_expected, double maxDeviation, double tol=1e-12) {
+        cs_ = wktreader_.read<CircularString>(wkt_in);
+        auto seq = cs_->getCoordinatesRO();
+        geos::geom::CircularArc arc(*seq, 0);
+        double radius = arc.getRadius();
+
+        double stepSizeDegrees = getDegreesFromSagitta(radius, maxDeviation);
+        checkLinearize(wkt_in, wkt_expected, stepSizeDegrees, tol);
+    }
+
+    void checkLinearize(const std::string& wkt_in, const std::string& wkt_expected, double stepSizeDegrees, double tol=1e-12) {
+        cs_ = wktreader_.read<CircularString>(wkt_in);
+
+        auto ls = cs_->getLinearized(stepSizeDegrees);
+
+        auto expected = wktreader_.read(wkt_expected);
+
+        ensure_equals_exact_geometry_xyzm(ls.get(), expected.get(), tol);
+
+        auto csRev = cs_->reverse();
+        auto lsRev = csRev->getLinearized(stepSizeDegrees);
+        auto lsRevRev = lsRev->reverse();
+
+        ensure_equals_exact_geometry_xyzm(lsRevRev.get(), expected.get(), tol);
+    }
+
+    static double getDegreesFromSagitta(double radius, double sagitta) {
+        return std::acos(1 - sagitta / radius) * 360 / geos::MATH_PI;
+    }
 };
 
 typedef test_group<test_circularstring_data> group;
@@ -289,4 +320,195 @@ void object::test<8>()
               "CIRCULARSTRING (-2 2, -3 5, 0 10, -5 5, 0 0)");
 }
 
+template<>
+template<>
+void object::test<9>()
+{
+    set_test_name("half-circle, evenly divisible by requested step size");
+
+    // expected result from GDAL 3.12
+    checkLinearize("CIRCULARSTRING (0 0, 1 1, 2 0)",
+    "LINESTRING (0 0, 0.002435949740176 0.069756473744125, 0.009731931258429 0.139173100960061, 0.021852399266194 0.20791169081776, 0.038738304061681 0.275637355817011, 0.060307379214091 0.342020143325669, 0.086454542357401 0.406736643075803, 0.117052407141074 0.469471562785898, 0.151951903843575 0.529919264233229, 0.190983005625057 0.587785252292491, 0.233955556881021 0.642787609686564, 0.280660199661355 0.694658370459024, 0.330869393641151 0.743144825477401, 0.384338524674348 0.788010753606727, 0.440807096529255 0.829037572555052, 0.5 0.866025403784448, 0.561628853210948 0.898794046299173, 0.625393406584095 0.927183854566806, 0.690983005625071 0.951056516295154, 0.75807810440034 0.970295726276021, 0.826351822333095 0.984807753012234, 0.895471536732373 0.99452189536828, 0.965100503297521 0.999390827019113, 1.03489949670251 0.999390827019113, 1.10452846326768 0.99452189536828, 1.17364817766696 0.984807753012234, 1.24192189559972 0.970295726276021, 1.30901699437499 0.951056516295182,
  1.37460659341593 0.927183854566806, 1.43837114678911 0.898794046299173, 1.50000000000006 0.866025403784448, 1.55919290347077 0.829037572555052, 1.61566147532568 0.788010753606727, 1.66913060635886 0.743144825477401, 1.71933980033867 0.694658370459024, 1.76604444311903 0.642787609686564, 1.80901699437499 0.587785252292491, 1.84804809615645 0.529919264233229, 1.88294759285895 0.469471562785898, 1.91354545764261 0.406736643075803, 1.93969262078593 0.342020143325669, 1.96126169593833 0.275637355817011, 1.97814760073385 0.20791169081776, 1.99026806874161 0.139173100960068, 1.99756405025983 0.069756473744128, 2 0)",
+    4);
+}
+
+template<>
+template<>
+void object::test<10>()
+{
+    set_test_name("half-circle, not divisible by requested step size");
+
+    // expected result from PostGIS 3.6
+    // SELECT ST_AsText(ST_CurveToLine('CIRCULARSTRING (0 0, 1 1, 2 0)', radians(23), 2, 1))
+    checkLinearize("CIRCULARSTRING (0 0, 1 1, 2 0)",
+    "LINESTRING(0 0,0.076120467488713 0.38268343236509,0.292893218813453 0.707106781186548,0.61731656763491 0.923879532511287,1 1,1.38268343236509 0.923879532511287,1.707106781186548 0.707106781186548,1.923879532511287 0.38268343236509,2 0)",
+    23);
+}
+
+template<>
+template<>
+void object::test<11>()
+{
+    set_test_name("liblwgeom: 2 segments per quadrant");
+    // SELECT ST_AsText(ST_CurveToLine('CIRCULARSTRING (0 0, 100 100, 200 0)', radians(45), 2, 1), 4);
+    checkLinearize("CIRCULARSTRING(0 0,100 100,200 0)",
+        "LINESTRING(0 0,29.2893 70.7107,100 100,170.7107 70.7107,200 0)", 90.0 / 2, 1e-4);
+}
+
+template<>
+template<>
+void object::test<12>()
+{
+    set_test_name("liblwgeom: 3 segments per quadrant");
+    // SELECT ST_AsText(ST_CurveToLine('CIRCULARSTRING (0 0, 100 100, 200 0)', radians(90.0 / 3), 2, 1), 4);
+    checkLinearize("CIRCULARSTRING(0 0,100 100,200 0)",
+        "LINESTRING(0 0,13.3975 50,50 86.6025,100 100,150 86.6025,186.6025 50,200 0)", 90.0 / 3, 1e-4);
+}
+
+template<>
+template<>
+void object::test<13>()
+{
+    set_test_name("liblwgeom: 2 segments per quadrant");
+    // SELECT ST_AsText(ST_CurveToLine('CIRCULARSTRING(29.2893218813453 70.7106781186548,100 100,200 0)', radians(90.0 / 2), 2, 1), 4);
+    checkLinearize("CIRCULARSTRING(29.2893218813453 70.7106781186548,100 100,200 0)",
+                "LINESTRING(29.2893 70.7107,100 100,170.7107 70.7107,200 0)", 90.0 / 2, 1e-4);
+}
+
+template<>
+template<>
+void object::test<14>()
+{
+    set_test_name("liblwgeom: 3 segments per quadrant - symmetric");
+    // SELECT ST_AsText(ST_CurveToLine('CIRCULARSTRING(29.2893218813453 70.7106781186548,100 100,200 0)', radians(90.0 / 3), 2, 1), 4);
+    checkLinearize("CIRCULARSTRING(29.2893218813453 70.7106781186548,100 100,200 0)",
+        "LINESTRING(29.2893 70.7107,69.0983 95.1057,115.6434 98.7688,158.7785 80.9017,189.1007 45.399,200 0)", 90.0 / 3, 1e-4);
+}
+
+template<>
+template<>
+void object::test<15>() {
+    set_test_name("liblwgeom: 10 segments per quadrant - circular");
+    auto cs = wktreader_.read<CircularString>("CIRCULARSTRING (0 0, 1 0, 0 0)");
+    auto ls = cs->getLinearized(90.0 / 10);
+    ensure_equals(ls->getNumPoints(), 41u); // PostGIS test has 40, but this is incorrect
+    const auto* seq = ls->getCoordinatesRO();
+    seq->forEachSegment([](const auto& p0, const auto& p1) {
+        ensure_equals("segment has expected length", p0.distance(p1), geos::MATH_PI/40, 0.01);
+    });
+}
+
+template<>
+template<>
+void object::test<16>() {
+    set_test_name("liblwgeom: maximum 10 units difference, symmetric");
+    checkLinearizeMaxDeviation("CIRCULARSTRING(0 0,100 100,200 0)",
+        "LINESTRING(0 0,30 70,100 100,170 70,200 0)", 10, 1.4);
+}
+
+template<>
+template<>
+void object::test<17>() {
+    set_test_name("liblwgeom: maximum 20 units difference, symmetric");
+    checkLinearizeMaxDeviation("CIRCULARSTRING(0 0,100 100,200 0)",
+        "LINESTRING(0 0,50 86,150 86,200 0)", 20, 1.4);
+}
+
+template<>
+template<>
+void object::test<18>() {
+    set_test_name("liblwgeom: ticket #3772 (1)");
+    checkLinearizeMaxDeviation("CIRCULARSTRING(71.96 -65.64,22.2 -18.52,20 50)",
+    "LINESTRING(72 -66,34 -38,16 4,20 50)", 4, 1.4);
+}
+
+template<>
+template<>
+void object::test<19>() {
+    set_test_name("liblwgeom: ticket #3772 (2)");
+    checkLinearizeMaxDeviation("CIRCULARSTRING(20 50,22.2 -18.52,71.96 -65.64)",
+    "LINESTRING(20 50,16 4,34 -38,72 -66)", 4, 1.4);
+}
+
+template<>
+template<>
+void object::test<20>() {
+    set_test_name("liblwgeom: ticket #4031, max deviation > 2*radius");
+    checkLinearizeMaxDeviation("CIRCULARSTRING(20 50,22.2 -18.52,71.96 -65.64)",
+    "LINESTRING(20 50,22 -18,72 -66)", 500, 1.4);
+}
+
+template<>
+template<>
+void object::test<21>() {
+    //set_test_name("liblwgeom: ticket #4058, big radius, small tolerance");
+    //checkLinearizeMaxDeviation("CIRCULARSTRING(2696000.553 1125699.831999999936670, 2695950.552000000141561 1125749.833000000100583, 2695865.195999999996275 1125835.189000)",
+    //"LINESTRING(2696000 1125700,2695932 1125768,2695866 1125836)", 0.0001);
+}
+
+template<>
+template<>
+void object::test<22>() {
+    set_test_name("liblwgeom: direction neutrality");
+    auto cs = wktreader_.read("CIRCULARSTRING(71.96 -65.64,22.2 -18.52,20 50)");
+    auto csRev = cs->reverse();
+
+    auto ls1 = cs->getLinearized(90.0 / 4);
+    auto ls2 = cs->reverse()->getLinearized(90.0 / 4)->reverse();
+
+    ensure("Linearization of reversed CIRCULARSTRING is not direction neutral", ls1->equalsExact(ls2.get(), 0.0));
+}
+
+template<>
+template<>
+void object::test<23>() {
+    set_test_name("getLinearized() called on linear arc");
+
+    checkLinearize("CIRCULARSTRING(0 0, 2 1, 4 2)",
+        "LINESTRING (0 0, 2 1, 4 2)", 90.0 / 4);
+}
+
+template<>
+template<>
+void object::test<24>() {
+    set_test_name("getLinearized() called on multi-section CircularString");
+
+    checkLinearize("CIRCULARSTRING (0 0, 1 1, 2 0, 3 -1, 4 0)",
+    "LINESTRING (0 0, 0.0761 0.3827, 0.2929 0.7071, 0.6173 0.9239, 1 1, 1.3827 0.9239, 1.7071 0.7071, 1.9239 0.3827, 2 0, 2.0761 -0.3827, 2.2929 -0.7071, 2.6173 -0.9239, 3 -1, 3.3827 -0.9239, 3.7071 -0.7071, 3.9239 -0.3827, 4 0)",
+    90.0 / 4, 1e-4);
+}
+
+template<>
+template<>
+void object::test<25>()
+{
+    set_test_name("getLinearized() on various CircularString base classes");
+
+    auto cs = wktreader_.read<CircularString>("CIRCULARSTRING(0 0, 1 1, 2 0)");
+
+    // check that we return LineString* rather than Curve* or Geometry*
+    std::unique_ptr<LineString> linearized = cs->getLinearized(45);
+
+    ensure_equals("CircularString::getLinearized", linearized.get()->getGeometryTypeId(), geos::geom::GEOS_LINESTRING);
+    ensure_equals("SimpleCurve::getLinearized", static_cast<geos::geom::SimpleCurve*>(cs.get())->getLinearized(45)->getGeometryTypeId(), geos::geom::GEOS_LINESTRING);
+    ensure_equals("Curve::getLinearized", static_cast<geos::geom::Curve*>(cs.get())->getLinearized(45)->getGeometryTypeId(), geos::geom::GEOS_LINESTRING);
+    ensure_equals("Geometry::getLinearized", static_cast<Geometry*>(cs.get())->getLinearized(45)->getGeometryTypeId(), geos::geom::GEOS_LINESTRING);
+}
+
+template<>
+template<>
+void object::test<26>()
+{
+    set_test_name("getCurved()");
+
+    auto cs = wktreader_.read<CircularString>("CIRCULARSTRING(0 0, 1 1, 2 0)");
+
+    // Check that we return Curve* rather than Geometry*
+    std::unique_ptr<geos::geom::Curve> curved = cs->getCurved(1000);
+
+    ensure_equals_exact_geometry_xyzm(curved.get(), cs.get(), 0);
+}
+
+// TODO: Test Z, M interpolation in getLinearized()
+
 }
diff --git a/tests/unit/geom/CompoundCurveTest.cpp b/tests/unit/geom/CompoundCurveTest.cpp
index 4d52c66ef..0a542dbde 100644
--- a/tests/unit/geom/CompoundCurveTest.cpp
+++ b/tests/unit/geom/CompoundCurveTest.cpp
@@ -10,9 +10,12 @@
 #include <geos/geom/CircularString.h>
 #include <geos/geom/IntersectionMatrix.h>
 #include <geos/io/WKTReader.h>
+#include <geos/operation/valid/RepeatedPointTester.h>
 #include <geos/util.h>
 
+
 using geos::geom::CompoundCurve;
+using geos::geom::Curve;
 using geos::geom::CoordinateXY;
 using geos::geom::CoordinateSequence;
 using geos::geom::SimpleCurve;
@@ -353,7 +356,7 @@ template<>
 template<>
 void object::test<9>()
 {
-    set_test_name("cannot create non-contiguous CompoundCurve");
+    set_test_name("construction failure on non-contiguous curves");
 
     std::vector<std::unique_ptr<SimpleCurve>> curves;
 
@@ -412,4 +415,40 @@ void object::test<11>()
     "COMPOUNDCURVE (CIRCULARSTRING (-10 0, -5 5, 0 0), (0 0, 10 0), CIRCULARSTRING (10 0, 0 -10, -10 0))");
 }
 
+template<>
+template<>
+void object::test<12>()
+{
+    set_test_name("getLinearized()");
+
+    auto cc = wktreader_.read("COMPOUNDCURVE (CIRCULARSTRING(-5 0, 0 5, 4 3, 5 0, 0 -5), (0 -5, -5 0))");
+    auto ls = cc->getLinearized(90.0 / 4);
+
+    ensure_equals(ls->getGeometryTypeId(), geos::geom::GEOS_LINESTRING);
+    ensure_equals(static_cast<geos::geom::Curve*>(cc.get())->getLinearized(90.0 / 4)->getGeometryTypeId(), geos::geom::GEOS_LINESTRING);
+    ensure_equals(static_cast<CompoundCurve*>(ls.get())->getLinearized(90.0 / 4)->getGeometryTypeId(), geos::geom::GEOS_LINESTRING);
+
+    geos::operation::valid::RepeatedPointTester rpt;
+    ensure(!rpt.hasRepeatedPoint(ls.get()));
+
+    auto expected = wktreader_.read("LINESTRING (-5 0, -4.685 1.7467, -3.7796 3.2733, -2.3979 4.3875, -0.7141 4.9487, 1.0597 4.8864, 2.6999 4.2084, 4 3, 4.8129 1.3551, 4.9776 -0.4723, 4.4721 -2.2361, 3.3644 -3.6987, 1.8036 -4.6634, 0 -5, -5 0)");
+    ensure_equals_exact_geometry(static_cast<Geometry*>(ls.get()), expected.get(), 1e-4);
+
+    auto ccRev = cc->reverse();
+    auto lsRev = ccRev->getLinearized(90.0 / 4);
+    ensure_equals_exact_geometry(static_cast<Geometry*>(lsRev->reverse().get()), expected.get(), 1e-4);
+}
+
+template<>
+template<>
+void object::test<13>()
+{
+    set_test_name("getCurved()");
+
+    // check that we return Curve* rather than Geometry*
+    std::unique_ptr<Curve> curved = cc_->getCurved(100.0);
+
+    ensure_equals_exact_geometry_xyzm(curved.get(), cc_.get(), 0);
+}
+
 }
diff --git a/tests/unit/geom/CurvePolygonTest.cpp b/tests/unit/geom/CurvePolygonTest.cpp
index 012fa8305..0c3ded6cd 100644
--- a/tests/unit/geom/CurvePolygonTest.cpp
+++ b/tests/unit/geom/CurvePolygonTest.cpp
@@ -278,4 +278,31 @@ void object::test<6>()
     ensure_equals_exact_geometry_xyzm(result.get(), expected.get(), 0);
 }
 
+template<>
+template<>
+void object::test<7>()
+{
+    set_test_name("getLinearized");
+
+    auto cp = wktreader_.read<CurvePolygon>("CURVEPOLYGON (COMPOUNDCURVE (CIRCULARSTRING (0 0, 2 0, 2 1, 2 3, 4 3), (4 3, 4 5, 1 4, 0 0)), CIRCULARSTRING (1.7 1, 1.4 0.4, 1.6 0.4, 1.6 0.5, 1.7 1))");
+
+    // check that we return Polygon* rather than Geometry*
+    std::unique_ptr<Polygon> poly = cp->getLinearized(90.0 / 4);
+
+    auto expected = wktreader_.read("POLYGON ((0 0, 0.2675 -0.3446, 0.6464 -0.5607, 1.0793 -0.6152, 1.5 -0.5, 1.8446 -0.2325, 2.0607 0.1464, 2.1152 0.5793, 2 1, 1.6934 1.4588, 1.5858 2, 1.6934 2.5412, 2 3, 2.4588 3.3066, 3 3.4142, 3.5412 3.3066, 4 3, 4 5, 1 4, 0 0), (1.7 1, 1.5871 1.0537, 1.4623 1.0629, 1.3427 1.0265, 1.2444 0.9492, 1.1806 0.8416, 1.16 0.7183, 1.1855 0.5958, 1.2534 0.4908, 1.3548 0.4175, 1.4757 0.3858, 1.6 0.4, 1.6203 0.705, 1.7 1))");
+
+    ensure_equals_exact_geometry_xyzm(poly.get(), expected.get(), 1e-4);
+}
+
+template<>
+template<>
+void object::test<8>()
+{
+    set_test_name("getCurved");
+
+    std::unique_ptr<CurvePolygon> curved = cp_->getCurved(1000);
+
+    ensure_equals_exact_geometry_xyzm(curved.get(), cp_.get(), 0);
+}
+
 }
diff --git a/tests/unit/geom/GeometryCollectionTest.cpp b/tests/unit/geom/GeometryCollectionTest.cpp
index 41477bb2f..c144059ac 100644
--- a/tests/unit/geom/GeometryCollectionTest.cpp
+++ b/tests/unit/geom/GeometryCollectionTest.cpp
@@ -181,4 +181,32 @@ void object::test<9>
     ensure(gc->hasDimension(geos::geom::Dimension::A));
 }
 
+template<>
+template<>
+void object::test<10>() {
+    set_test_name("getCurved()");
+
+    auto gc = readWKT("GEOMETRYCOLLECTION(POINT (3 7), CIRCULARSTRING (0 0, 1 1, 2 0), LINESTRING (1 1, 2 3))");
+
+    auto linearized = gc->getLinearized(45);
+
+    auto expected = readWKT("GEOMETRYCOLLECTION(POINT (3 7), LINESTRING (0 0, 0.292893 0.707107, 1 1, 1.707107 0.707107, 2 0), LINESTRING (1 1, 2 3))");
+
+    ensure_equals_exact_geometry(linearized.get(), expected.get(), 1e-4);
+}
+
+template<>
+template<>
+void object::test<11>() {
+    set_test_name("getLinearized()");
+
+    auto gc = readWKT("GEOMETRYCOLLECTION(POINT (3 7), LINESTRING (0 0, 0.292893 0.707107, 1 1, 1.707107 0.707107, 2 0), LINESTRING (1 1, 2 3))");
+
+    auto curved = gc->getCurved(1e-3);
+
+    auto expected = readWKT("GEOMETRYCOLLECTION(POINT (3 7), CIRCULARSTRING (0 0, 1 1, 2 0), LINESTRING (1 1, 2 3))");
+
+    ensure_equals_exact_geometry(curved.get(), expected.get(), 1e-4);
+}
+
 } // namespace tut
diff --git a/tests/unit/geom/LineStringTest.cpp b/tests/unit/geom/LineStringTest.cpp
index 33d74af9d..90edafcb3 100644
--- a/tests/unit/geom/LineStringTest.cpp
+++ b/tests/unit/geom/LineStringTest.cpp
@@ -643,6 +643,38 @@ void object::test<35>
     ensure_equals(out, "POINT ZM (20 21 22 23)");
 }
 
+template<>
+template<>
+void object::test<36>()
+{
+    set_test_name("getLinearized");
+
+    // check that we return LineString* rather than Curve* or Geometry*
+    std::unique_ptr<LineString> linearized = line_->getLinearized(1e-3);
+
+    ensure_equals_exact_geometry_xyzm(linearized.get(), line_.get(), 0);
+}
+
+template<>
+template<>
+void object::test<37>()
+{
+    set_test_name("getCurved");
+
+    WKTReader reader;
+
+    auto input = reader.read<LineString>("LINESTRING (2 2, 2.292893 2.707107, 3 3, 3.707107 2.707107, 4 2, 2 2)");
+
+    // check that we return Curve* instead of Geometry*
+    std::unique_ptr<geos::geom::Curve> curved = input->getCurved(1e-3);
+
+    ensure_equals(curved->getGeometryTypeId(), geos::geom::GEOS_COMPOUNDCURVE);
+
+    auto expected = reader.read("COMPOUNDCURVE (CIRCULARSTRING (2 2, 3 3, 4 2), (4 2, 2 2))");
+
+    ensure_equals_exact_geometry_xyzm(curved.get(), expected.get(), 1e-6);
+}
+
 
 } // namespace tut
 
diff --git a/tests/unit/geom/MultiCurveTest.cpp b/tests/unit/geom/MultiCurveTest.cpp
index 9973b5177..316c22b5d 100644
--- a/tests/unit/geom/MultiCurveTest.cpp
+++ b/tests/unit/geom/MultiCurveTest.cpp
@@ -203,4 +203,27 @@ void object::test<4>()
     ensure(!wktreader_.read<geos::geom::MultiCurve>("MULTICURVE ((0 0, 1 0, 1 1, 0 0), CIRCULARSTRING (3 3, 4 4, 5 3))")->isClosed());
 }
 
+template<>
+template<>
+void object::test<5>()
+{
+    set_test_name("getLinearized()");
+
+    // check that we return MultiLineString*, not Geometry*
+    std::unique_ptr<geos::geom::MultiLineString> mls = mc_->getLinearized(2);
+
+    ensure_equals(mls->getGeometryTypeId(), geos::geom::GEOS_MULTILINESTRING);
+    ensure_equals("getLength()", mls->getLength(), mc_->getLength(), 1e-3);
+}
+
+template<>
+template<>
+void object::test<6>()
+{
+    set_test_name("getCurved()");
+
+    std::unique_ptr<geos::geom::GeometryCollection> curved = mc_->getCurved(100);
+    ensure_equals_exact_geometry_xyzm(mc_.get(), curved.get(), 0);
+}
+
 }
diff --git a/tests/unit/geom/MultiLineStringTest.cpp b/tests/unit/geom/MultiLineStringTest.cpp
index 7e7433a9d..361e884fd 100644
--- a/tests/unit/geom/MultiLineStringTest.cpp
+++ b/tests/unit/geom/MultiLineStringTest.cpp
@@ -7,6 +7,10 @@
 #include <geos/geom/MultiLineString.h>
 #include <geos/io/WKTReader.h>
 
+#include "utility.h"
+
+using geos::geom::MultiLineString;
+
 namespace tut {
 //
 // Test Group
@@ -14,14 +18,14 @@ namespace tut {
 
 // Common data used by tests
 struct test_multilinestring_data {
-    std::unique_ptr<geos::geom::Geometry> empty_mls_;
-    std::unique_ptr<geos::geom::Geometry> mls_;
+    std::unique_ptr<MultiLineString> empty_mls_;
+    std::unique_ptr<MultiLineString> mls_;
 
     geos::io::WKTReader reader_;
 
     test_multilinestring_data() {
-        empty_mls_ = reader_.read("MULTILINESTRING EMPTY");
-        mls_ = reader_.read("MULTILINESTRING ((0 0, 1 1), (3 3, 4 4))");
+        empty_mls_ = reader_.read<MultiLineString>("MULTILINESTRING EMPTY");
+        mls_ = reader_.read<MultiLineString>("MULTILINESTRING ((0 0, 1 1), (3 3, 4 4))");
     }
 };
 
@@ -75,6 +79,38 @@ void object::test<4>
     ensure(!mls_->hasDimension(geos::geom::Dimension::A));
 }
 
+template<>
+template<>
+void object::test<5>()
+{
+    set_test_name("getLinearized()");
+
+    // check that return MultiLineString*, not Geometry*
+    std::unique_ptr<MultiLineString> linearized = mls_->getLinearized(1.0);
+
+    ensure_equals_exact_geometry_xyzm(linearized.get(), mls_.get(), 0);
+}
+
+template<>
+template<>
+void object::test<6>()
+{
+    set_test_name("getCurved");
+
+    auto input = reader_.read<MultiLineString>("MULTILINESTRING ((3 3, 4 4), (-2 0, -1.414 1.414, 0 2, 1.414 1.414, 2 0, 2 3))");
+
+    ensure_equals(input->getCurved(1e-9)->getGeometryTypeId(), geos::geom::GEOS_MULTILINESTRING);
+
+    // check that we return GeometryCollection*, not Geometry*
+    std::unique_ptr<geos::geom::GeometryCollection> curved = input->getCurved(2e-3);
+
+    ensure_equals(curved->getGeometryTypeId(), geos::geom::GEOS_MULTICURVE);
+
+    auto expected = reader_.read("MULTICURVE ((3 3, 4 4), COMPOUNDCURVE(CIRCULARSTRING (-2 0, 0 2, 2 0), (2 0, 2 3)))");
+
+    ensure_equals_exact_geometry_xyzm(curved.get(), expected.get(), 1e-3);
+}
+
 
 } // namespace tut
 
diff --git a/tests/unit/geom/MultiPolygonTest.cpp b/tests/unit/geom/MultiPolygonTest.cpp
index efe46919c..df437bda1 100644
--- a/tests/unit/geom/MultiPolygonTest.cpp
+++ b/tests/unit/geom/MultiPolygonTest.cpp
@@ -7,6 +7,8 @@
 #include <geos/geom/MultiPolygon.h>
 #include <geos/io/WKTReader.h>
 
+#include "utility.h"
+
 namespace tut {
 //
 // Test Group
@@ -73,4 +75,36 @@ void object::test<4>
     ensure(mp_->hasDimension(geos::geom::Dimension::A));
 }
 
+template<>
+template<>
+void object::test<5>()
+{
+    set_test_name("correct type returned by getLinearized");
+
+    ensure(mp_->getLinearized(0)->getGeometryTypeId() == geos::geom::GeometryTypeId::GEOS_MULTIPOLYGON);
+}
+
+template<>
+template<>
+void object::test<6>()
+{
+    set_test_name("getCurved");
+
+    auto input = reader_.read(
+        "MULTIPOLYGON (((0 0, 10 0, 10 10, 0 10, 0 0), (2 2, 2.292893 2.707107, 3 3, 3.707107 2.707107, 4 2, 2 2)),"
+                                     "((20 0, 30 0, 30 10, 20 0)))");
+
+    // Tolerance to small to allow conversion to MultiSurface
+    ensure_equals(input->getCurved(1e-12)->getGeometryTypeId(), geos::geom::GeometryTypeId::GEOS_MULTIPOLYGON);
+
+    auto curved = input->getCurved(0.001);
+
+    ensure_equals(curved->getGeometryTypeId(), geos::geom::GeometryTypeId::GEOS_MULTISURFACE);
+
+    auto expected = reader_.read("MULTISURFACE (CURVEPOLYGON ((0 0, 10 0, 10 10, 0 10, 0 0), COMPOUNDCURVE (CIRCULARSTRING(2 2, 3 3, 4 2), (4 2, 2 2))), ((20 0, 30 0, 30 10, 20 0)))");
+
+    ensure_equals_exact_geometry_xyzm(curved.get(), expected.get(), 1e-3);
+}
+
+
 } // namespace tut
diff --git a/tests/unit/geom/MultiSurfaceTest.cpp b/tests/unit/geom/MultiSurfaceTest.cpp
index 9ee1cc244..61abec09f 100644
--- a/tests/unit/geom/MultiSurfaceTest.cpp
+++ b/tests/unit/geom/MultiSurfaceTest.cpp
@@ -174,4 +174,29 @@ void object::test<3>()
     auto cc3 = ms_->reverse();
 }
 
+template<>
+template<>
+void object::test<4>()
+{
+    set_test_name("getLinearized()");
+
+    // check that we return MultiPolygon*
+    std::unique_ptr<geos::geom::MultiPolygon> mp_ = ms_->getLinearized(2);
+
+    ensure_equals(mp_->getGeometryTypeId(), geos::geom::GEOS_MULTIPOLYGON);
+}
+
+template<>
+template<>
+void object::test<5>()
+{
+    set_test_name("getCurved()");
+
+    std::unique_ptr<geos::geom::GeometryCollection> curved = ms_->getCurved(100);
+
+    ensure_equals_exact_geometry_xyzm(ms_.get(), curved.get(), 0);
+}
+
+
+
 }
diff --git a/tests/unit/geom/PointTest.cpp b/tests/unit/geom/PointTest.cpp
index fc13bf9bc..d9720c094 100644
--- a/tests/unit/geom/PointTest.cpp
+++ b/tests/unit/geom/PointTest.cpp
@@ -18,6 +18,8 @@
 #include <memory>
 #include <string>
 
+#include "utility.h"
+
 constexpr int MAX_TESTS = 100;
 
 namespace tut {
@@ -630,5 +632,23 @@ void object::test<48>
     ensure(!point_->hasDimension(geos::geom::Dimension::A));
 }
 
+template<>
+template<>
+void object::test<49>()
+{
+    set_test_name("getLinearized");
+
+    ensure_equals_exact_geometry_xyzm(point_.get(), point_->getLinearized(4).get(), 0);
+}
+
+template<>
+template<>
+void object::test<50>()
+{
+    set_test_name("getCurved");
+
+    ensure_equals_exact_geometry_xyzm(point_.get(), point_->getCurved(1e-3).get(), 0);
+}
+
 } // namespace tut
 
diff --git a/tests/unit/geom/PolygonTest.cpp b/tests/unit/geom/PolygonTest.cpp
index 24cafef0c..e57d33638 100644
--- a/tests/unit/geom/PolygonTest.cpp
+++ b/tests/unit/geom/PolygonTest.cpp
@@ -748,4 +748,35 @@ void object::test<47>
     ensure(empty_poly_zm_->hasM());
 }
 
+template<>
+template<>
+void object::test<49>()
+{
+    set_test_name("getLinearized");
+
+    // check that we return Polygon* rather than Geometry*
+    std::unique_ptr<Polygon> linearized = poly_zm_->getLinearized(4);
+
+    ensure_equals_exact_geometry_xyzm(linearized.get(), poly_zm_.get(),  0);
+}
+
+template<>
+template<>
+void object::test<50>()
+{
+    set_test_name("getCurved");
+
+    WKTReader reader;
+    auto input = reader.read<Polygon>("POLYGON ((0 0, 0.2675 -0.3446, 0.6464 -0.5607, 1.0793 -0.6152, 1.5 -0.5, 1.8446 -0.2325, 2.0607 0.1464, 2.1152 0.5793, 2 1, 1.6934 1.4588, 1.5858 2, 1.6934 2.5412, 2 3, 2.4588 3.3066, 3 3.4142, 3.5412 3.3066, 4 3, 4 5, 1 4, 0 0), (1.7 1, 1.5871 1.0537, 1.4623 1.0629, 1.3427 1.0265, 1.2444 0.9492, 1.1806 0.8416, 1.16 0.7183, 1.1855 0.5958, 1.2534 0.4908, 1.3548 0.4175, 1.4757 0.3858, 1.6 0.4, 1.6203 0.705, 1.7 1))");
+
+    // check that we return Surface* rather than Geometry*
+    std::unique_ptr<geos::geom::Surface> curved = input->getCurved(0.01);
+
+    ensure_equals(curved->getGeometryTypeId(), geos::geom::GeometryTypeId::GEOS_CURVEPOLYGON);
+
+    auto expected = reader.read("CURVEPOLYGON (COMPOUNDCURVE (CIRCULARSTRING (0 0, 1.501026632988956 -0.500132555776532, 2 1, 1.999860522668543 2.999400752659415, 4 3), (4 3, 4 5, 1 4, 0 0)), COMPOUNDCURVE (CIRCULARSTRING (1.7 1, 1.16517094963354 0.7813913411683361, 1.6 0.4), (1.6 0.4, 1.6203 0.705, 1.7 1)))");
+
+    ensure_equals_exact_geometry_xyzm(curved.get(), expected.get(), 1e-2);
+}
+
 } // namespace tut
diff --git a/tests/unit/utility.h b/tests/unit/utility.h
index a4f522831..69acdee86 100644
--- a/tests/unit/utility.h
+++ b/tests/unit/utility.h
@@ -451,6 +451,7 @@ ensure_equals_exact_geometry_xyzm(const geos::geom::Geometry *lhs_in,
     assert(nullptr != rhs_in);
 
     using geos::geom::Point;
+    using geos::geom::CompoundCurve;
     using geos::geom::Curve;
     using geos::geom::CompoundCurve;
     using geos::geom::SimpleCurve;
@@ -556,6 +557,9 @@ ensure_equals_exact_geometry(const geos::geom::Geometry *lhs_in,
     assert(nullptr != lhs_in);
     assert(nullptr != rhs_in);
 
+    using geos::geom::Point;
+    using geos::geom::SimpleCurve;
+    using geos::geom::Surface;
     using geos::geom::CoordinateSequence;
     using geos::geom::Curve;
     using geos::geom::GeometryCollection;

-----------------------------------------------------------------------

Summary of changes:
 capi/CMakeLists.txt                                |  10 +
 capi/geos_c.cpp                                    |  69 +++
 capi/geos_c.h.in                                   | 484 ++++++++++++++++--
 capi/geos_ts_c.cpp                                 | 562 ++++++++++++++++-----
 include/geos/algorithm/CircularArcs.h              |   3 +
 include/geos/algorithm/CurveToLineParams.h         |  76 +++
 include/geos/algorithm/LineToCurveConverter.h      |  74 +++
 include/geos/algorithm/LineToCurveParams.h         |  79 +++
 include/geos/geom/CircularArc.h                    |   9 +
 include/geos/geom/CircularString.h                 |   9 +
 include/geos/geom/CompoundCurve.h                  |   5 +
 include/geos/geom/Curve.h                          |   9 +
 include/geos/geom/CurvePolygon.h                   |  13 +-
 include/geos/geom/Geometry.h                       |  22 +
 include/geos/geom/GeometryCollection.h             |  12 +
 include/geos/geom/LineString.h                     |   6 +
 include/geos/geom/MultiCurve.h                     |   9 +
 include/geos/geom/MultiLineString.h                |   8 +
 include/geos/geom/MultiPolygon.h                   |   5 +-
 include/geos/geom/MultiSurface.h                   |   8 +
 include/geos/geom/Point.h                          |   4 +
 include/geos/geom/Polygon.h                        |  10 +
 include/geos/geom/util/CurveBuilder.h              |   3 +
 src/algorithm/CircularArcs.cpp                     |   5 +
 src/algorithm/LineToCurveConverter.cpp             | 272 ++++++++++
 src/coverage/CoverageCleaner.cpp                   |   6 +-
 src/geom/CircularArc.cpp                           |  49 ++
 src/geom/CircularString.cpp                        |  27 +-
 src/geom/CompoundCurve.cpp                         |  26 +
 src/geom/Curve.cpp                                 |  17 +-
 src/geom/CurvePolygon.cpp                          |  25 +-
 src/geom/GeometryCollection.cpp                    |  26 +
 src/geom/LineString.cpp                            |  19 +-
 src/geom/MultiCurve.cpp                            |  12 +
 src/geom/MultiLineString.cpp                       |  29 +-
 src/geom/MultiPolygon.cpp                          |  21 +
 src/geom/MultiSurface.cpp                          |  14 +
 src/geom/Polygon.cpp                               |  37 +-
 src/geom/util/CurveBuilder.cpp                     |   6 +
 src/triangulate/DelaunayTriangulationBuilder.cpp   |   2 -
 src/triangulate/VoronoiDiagramBuilder.cpp          |   1 -
 tests/unit/algorithm/CircularArcsTest.cpp          |  15 +
 tests/unit/algorithm/LineToCurveConverterTest.cpp  | 308 +++++++++++
 tests/unit/capi/GEOSAreaTest.cpp                   |  35 ++
 tests/unit/capi/GEOSBufferTest.cpp                 |  43 +-
 tests/unit/capi/GEOSClipByRectTest.cpp             |  19 +-
 tests/unit/capi/GEOSClusterTest.cpp                |  58 +++
 tests/unit/capi/GEOSConcaveHullOfPolygonsTest.cpp  |  22 +-
 tests/unit/capi/GEOSConcaveHullTest.cpp            |  27 +-
 .../GEOSConstrainedDelaunayTriangulationTest.cpp   |  12 +-
 tests/unit/capi/GEOSContainsTest.cpp               |  13 +-
 tests/unit/capi/GEOSConvexHullTest.cpp             |  11 +-
 tests/unit/capi/GEOSCoverageCleanTest.cpp          |  60 +++
 tests/unit/capi/GEOSCoverageIsValidTest.cpp        |  19 +-
 tests/unit/capi/GEOSCoverageSimplifyTest.cpp       |  22 +-
 tests/unit/capi/GEOSCoverageUnionTest.cpp          |  22 +-
 tests/unit/capi/GEOSCoveredByTest.cpp              |  12 +-
 tests/unit/capi/GEOSCoversTest.cpp                 |  12 +-
 tests/unit/capi/GEOSCrossesTest.cpp                |  12 +-
 tests/unit/capi/GEOSCurveToLineTest.cpp            | 103 ++++
 tests/unit/capi/GEOSDelaunayTriangulationTest.cpp  |   7 +-
 tests/unit/capi/GEOSDensifyTest.cpp                |  10 +-
 tests/unit/capi/GEOSDifferenceTest.cpp             |   3 +-
 tests/unit/capi/GEOSDisjointSubsetUnionTest.cpp    |  30 +-
 tests/unit/capi/GEOSDisjointTest.cpp               |  16 +
 tests/unit/capi/GEOSEnvelopeTest.cpp               |  10 +
 tests/unit/capi/GEOSEqualsExactTest.cpp            |  46 ++
 tests/unit/capi/GEOSEqualsTest.cpp                 |  13 +-
 tests/unit/capi/GEOSFrechetDistanceTest.cpp        |  16 +-
 tests/unit/capi/GEOSGetCentroidTest.cpp            |  10 +-
 .../capi/GEOSGridIntersectionFractionsTest.cpp     |   9 +-
 tests/unit/capi/GEOSHausdorffDistanceTest.cpp      |  29 +-
 tests/unit/capi/GEOSInterpolateTest.cpp            |  32 ++
 tests/unit/capi/GEOSIntersectsTest.cpp             |  13 +-
 tests/unit/capi/GEOSLargestEmptyCircleTest.cpp     |  26 +-
 tests/unit/capi/GEOSLineToCurveTest.cpp            |  89 ++++
 tests/unit/capi/GEOSMakeValidTest.cpp              |  30 +-
 tests/unit/capi/GEOSMaximumInscribedCircleTest.cpp |  19 +-
 tests/unit/capi/GEOSMinimumBoundingCircleTest.cpp  |  12 +-
 tests/unit/capi/GEOSMinimumClearanceTest.cpp       |  27 +
 .../unit/capi/GEOSMinimumRotatedRectangleTest.cpp  |  13 +-
 tests/unit/capi/GEOSMinimumWidthTest.cpp           |  12 +-
 tests/unit/capi/GEOSNearestPointsTest.cpp          |  41 +-
 tests/unit/capi/GEOSOffsetCurveTest.cpp            |  11 +-
 tests/unit/capi/GEOSOverlapsTest.cpp               |  14 +-
 tests/unit/capi/GEOSPointOnSurfaceTest.cpp         |  11 +-
 tests/unit/capi/GEOSPolygonHullSimplifyTest.cpp    |  32 +-
 tests/unit/capi/GEOSPolygonizeTest.cpp             |   6 +-
 tests/unit/capi/GEOSProjectTest.cpp                |  24 +
 tests/unit/capi/GEOSRelateBoundaryNodeRuleTest.cpp |  27 +-
 tests/unit/capi/GEOSRelatePatternTest.cpp          |  13 +-
 tests/unit/capi/GEOSRelateTest.cpp                 |  14 +-
 tests/unit/capi/GEOSSharedPathsTest.cpp            |  15 +-
 tests/unit/capi/GEOSSubdivideByGridTest.cpp        |  23 +
 tests/unit/capi/GEOSTouchesTest.cpp                |  17 +
 tests/unit/capi/GEOSUnaryUnionTest.cpp             |  33 +-
 tests/unit/capi/GEOSUnionTest.cpp                  |   1 +
 tests/unit/capi/GEOSVoronoiDiagramTest.cpp         |   4 +-
 tests/unit/capi/GEOSWithinTest.cpp                 |  12 +-
 tests/unit/capi/GEOSisRingTest.cpp                 |  10 +-
 tests/unit/capi/GEOSisSimpleDetailTest.cpp         |   7 +-
 tests/unit/capi/GEOSisSimpleTest.cpp               |   8 +-
 tests/unit/capi/GEOSisValidDetailTest.cpp          |  10 +-
 tests/unit/capi/GEOSisValidReasonTest.cpp          |  15 +-
 tests/unit/capi/GEOSisValidTest.cpp                |   8 +-
 tests/unit/capi/capi_test_utils.h                  | 106 +++-
 tests/unit/geom/CircularStringTest.cpp             | 246 +++++++++
 tests/unit/geom/CompoundCurveTest.cpp              |  45 +-
 tests/unit/geom/CurvePolygonTest.cpp               |  29 ++
 tests/unit/geom/GeometryCollectionTest.cpp         |  36 ++
 tests/unit/geom/LineStringTest.cpp                 |  37 ++
 tests/unit/geom/MultiCurveTest.cpp                 |  25 +
 tests/unit/geom/MultiLineStringTest.cpp            |  51 +-
 tests/unit/geom/MultiPolygonTest.cpp               |  40 ++
 tests/unit/geom/MultiSurfaceTest.cpp               |  27 +
 tests/unit/geom/PointTest.cpp                      |  22 +
 tests/unit/geom/PolygonTest.cpp                    |  36 ++
 tests/unit/utility.h                               |   4 +
 118 files changed, 4078 insertions(+), 362 deletions(-)
 create mode 100644 include/geos/algorithm/CurveToLineParams.h
 create mode 100644 include/geos/algorithm/LineToCurveConverter.h
 create mode 100644 include/geos/algorithm/LineToCurveParams.h
 create mode 100644 src/algorithm/LineToCurveConverter.cpp
 create mode 100644 tests/unit/algorithm/LineToCurveConverterTest.cpp
 create mode 100644 tests/unit/capi/GEOSAreaTest.cpp
 create mode 100644 tests/unit/capi/GEOSCoverageCleanTest.cpp
 create mode 100644 tests/unit/capi/GEOSCurveToLineTest.cpp
 create mode 100644 tests/unit/capi/GEOSEqualsExactTest.cpp
 create mode 100644 tests/unit/capi/GEOSLineToCurveTest.cpp


hooks/post-receive
-- 
GEOS


More information about the geos-commits mailing list