[geos-commits] [SCM] GEOS branch main updated. 7f429a927aa7638a2087a59f62a5f67fe1d094a2

git at osgeo.org git at osgeo.org
Wed May 8 08:54:50 PDT 2024


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  7f429a927aa7638a2087a59f62a5f67fe1d094a2 (commit)
       via  415691cbafe016d401c7eebf83497a7da3785106 (commit)
       via  4e992e35c67e1b9138d472eff627b7f1aa3b5cff (commit)
       via  fcaebfc55ede926fa23419bee451c54e798d8c6d (commit)
       via  3ebc98177c002b9d1bb8a891272b9704631ed67e (commit)
       via  836a4d83d55a25ea88264dc690a5ce0670408744 (commit)
       via  9d415621b9563aa16eb6f3ab079c699ba8d1dae8 (commit)
       via  cbf94fef29d4c485df73a66bf4cae89d5a24b402 (commit)
       via  d4b654ac9d3b2507c81153cf54f7b0e5c3ff9833 (commit)
       via  11fae139b1641d6514bd56c7f216683056322317 (commit)
       via  d2c196b91d26267135c69165aab0f52adc26d10e (commit)
       via  c8b889be9e8fa22de8a34bea50fec3bb073f6898 (commit)
       via  9a5c9a5ae0181baf259da460572301d60fbfb193 (commit)
       via  60e0ae2b8f5d1df0d95cfe95eba2ba4f1cbd816e (commit)
       via  629c2f6939f30a4b19682a87915fb2457a90db3c (commit)
       via  abc9dd178306cafad367e478e9a6e8ff9bb293f7 (commit)
       via  ba59b3f38d962f8f0749657ca7a1b2ce74664097 (commit)
       via  d9bee63471dea5596c5aaa89eaa0a8512f937773 (commit)
       via  479541dddcdc680b9e27d2c1d77c099488d05c5c (commit)
       via  d12e2e68448b91d01bd7cfbdebaaee2f781c8a9a (commit)
       via  4fc18f5455244995827edde3adf258a5d820f0e8 (commit)
       via  ccedb1d89c7b3f94d272cda0b027513430cdd37e (commit)
       via  61152e4b065b6267882c1ee6a39f180187a92a27 (commit)
       via  365cb578445a6bd74a069cac8aa1705658a7772f (commit)
      from  5a71e20cb0a3295bb73e5e3449d0c1b7bfeb603f (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 7f429a927aa7638a2087a59f62a5f67fe1d094a2
Author: Sandro Santilli <strk at kbt.io>
Date:   Mon May 6 11:55:18 2024 +0200

    Report when using floating point overlay (in debug mode)

diff --git a/src/operation/overlayng/OverlayNGRobust.cpp b/src/operation/overlayng/OverlayNGRobust.cpp
index 96dda0f69..e9af20896 100644
--- a/src/operation/overlayng/OverlayNGRobust.cpp
+++ b/src/operation/overlayng/OverlayNGRobust.cpp
@@ -109,7 +109,9 @@ OverlayNGRobust::Overlay(const Geometry* geom0, const Geometry* geom1, int opCod
      */
     try {
         geom::PrecisionModel PM_FLOAT;
-        // std::cerr << "Using floating point overlay." << std::endl;
+#if GEOS_DEBUG
+        std::cerr << "Using floating point overlay." << std::endl;
+#endif
         result = OverlayNG::overlay(geom0, geom1, opCode, &PM_FLOAT);
 
         // Simple noding with no validation

commit 415691cbafe016d401c7eebf83497a7da3785106
Author: Sandro Santilli <strk at kbt.io>
Date:   Mon May 6 11:36:38 2024 +0200

    Fix versioning of CAPI library
    
    Closes GH-1091

diff --git a/Version.txt b/Version.txt
index 58a5da055..116c2526b 100644
--- a/Version.txt
+++ b/Version.txt
@@ -15,9 +15,9 @@ GEOS_PATCH_WORD=dev
 # - Deleting interfaces / compatibility issues - bump CURRENT, others to zero
 #   ( THIS MUST BE CAREFULLY AVOIDED )
 #
-CAPI_INTERFACE_CURRENT=19
+CAPI_INTERFACE_CURRENT=20
 CAPI_INTERFACE_REVISION=0
-CAPI_INTERFACE_AGE=18
+CAPI_INTERFACE_AGE=19
 
 # JTS Port
 JTS_PORT=1.18.0

commit 4e992e35c67e1b9138d472eff627b7f1aa3b5cff
Author: Martin Davis <mtnclimb at gmail.com>
Date:   Thu May 2 15:42:25 2024 -0700

    Update NEWS with info about OffsetCurve change in 3.11

diff --git a/NEWS.md b/NEWS.md
index 00a78583f..983cb3a7b 100644
--- a/NEWS.md
+++ b/NEWS.md
@@ -97,13 +97,13 @@
   - MaximumInscribedCircle: Fix infinite loop with non-finite coordinates (GH-843, Dan Baston)
   - DistanceOp: Fix crash on collection containing empty point (GH-842, Dan Baston)
   - OffsetCurve: improve behaviour and add Joined mode (JTS-956, Martin Davis)
-  - GeometryPrecisionReducer: preserve input collection types (GH-846, Paul Ramsey)
   - OffsetCurve: handle zero-distance offsets (GH-850, Martin Davis)
+  - OffsetCurve: fix EndCap parameter handling (GH-899, Martin Davis)
+  - GeometryPrecisionReducer: preserve input collection types (GH-846, Paul Ramsey)
   - Tri: add exceptions for invalid indexes (GH-853, Martin Davis)
   - LargestEmptyCircle: enhance boundary to allow any polygonal geometry (GH-859, Martin Davis)
   - Fix MaximumInscribedCircle and LargestEmptyCircle performance and memory issues (GH-883, Martin Davis)
   - GEOSHasZ: Fix handling with empty geometries (GH-887, Mike Taves)
-  - OffsetCurve: fix EndCap parameter handling (GH-899, Martin Davis)
   - Reduce artifacts in single-sided Buffers: (GH-665 #810 and #712, Sandro Santilli)
   - GeoJSONReader: Fix 2D empty geometry creation (GH-909, Mike Taves)
   - GEOSClipByRect: Fix case with POINT EMPTY (GH-913, Mike Taves)
@@ -156,6 +156,8 @@
   - Fix BufferOp inverted ring check (JTS-878, Martin Davis)
   - Fix OverlayNG geomunion to avoid lines in result (Martin Davis)
 
+- Changes:
+  - Offset curves now have the same direction as the input line, for both positive and negative offsets.
 
 ## Changes in 3.10.0
 2021-10-20

commit fcaebfc55ede926fa23419bee451c54e798d8c6d
Author: Daniel Baston <dbaston at gmail.com>
Date:   Tue Apr 30 13:47:16 2024 -0400

    Update NEWS

diff --git a/NEWS.md b/NEWS.md
index b378d9fca..00a78583f 100644
--- a/NEWS.md
+++ b/NEWS.md
@@ -3,6 +3,8 @@
 
 - New things:
   - Add Angle::sinCosSnap to avoid small errors, e.g. with buffer operations (GH-978, Mike Taves)
+  - Add classes for curved geometry types: CircularString, CompoundCurve, CurvedPolygon, MultiCurve,
+    MultiSurface (GH-1046, Dan Baston)
 
 - Breaking Changes
 

commit 3ebc98177c002b9d1bb8a891272b9704631ed67e
Merge: 9a5c9a5ae 836a4d83d
Author: Dan Baston <dbaston at gmail.com>
Date:   Tue Apr 30 09:29:16 2024 -0400

    Merge branch 'main' into curves


commit 836a4d83d55a25ea88264dc690a5ce0670408744
Author: Martin Davis <mtnclimb at gmail.com>
Date:   Wed Apr 24 21:05:27 2024 -0700

    Add doc for WKB POINT EMPTY handling

diff --git a/web/content/specifications/wkb.md b/web/content/specifications/wkb.md
index 585b2c0eb..4043353b5 100644
--- a/web/content/specifications/wkb.md
+++ b/web/content/specifications/wkb.md
@@ -73,6 +73,13 @@ LinearRing {
 };
 ```
 
+### Empty Geometries
+
+For most geometry types, empty geometries are indicated naturally by having `numPoints` = 0.
+Points do not have a `numPoints` field, so `POINT EMPTY` is represented by a `Point` 
+with each ordinate value set to an IEEE-754 quiet NaN value
+(big endian 0x7ff8000000000000 or little endian 0x000000000000f87f).
+
 ### Geometry Types
 
 GEOS only supports the seven original simple features geometry types.

commit 9d415621b9563aa16eb6f3ab079c699ba8d1dae8
Author: Dan Baston <dbaston at gmail.com>
Date:   Wed Apr 24 20:52:38 2024 -0400

    MinimumClearance: Avoid crash on NaN inputs (#1082)
    
    Resolves https://github.com/libgeos/geos/issues/1079

diff --git a/src/precision/MinimumClearance.cpp b/src/precision/MinimumClearance.cpp
index cc6b293d0..0a5e9e823 100644
--- a/src/precision/MinimumClearance.cpp
+++ b/src/precision/MinimumClearance.cpp
@@ -176,6 +176,10 @@ MinimumClearance::compute()
     MinClearanceDistance mcd;
     auto nearest = tree->nearestNeighbour(mcd);
 
+    if (nearest.first == nullptr || nearest.second == nullptr) {
+        throw util::GEOSException("Failed to find nearest items");
+    }
+
     minClearance = mcd.distance(nearest.first, nearest.second);
 
     const std::vector<Coordinate>* minClearancePtsVec = mcd.getCoordinates();
diff --git a/tests/unit/capi/GEOSMinimumClearanceTest.cpp b/tests/unit/capi/GEOSMinimumClearanceTest.cpp
index f683145da..e0c777afd 100644
--- a/tests/unit/capi/GEOSMinimumClearanceTest.cpp
+++ b/tests/unit/capi/GEOSMinimumClearanceTest.cpp
@@ -120,4 +120,14 @@ void object::test<5>
                   geos::DoubleInfinity);
 }
 
+template<>
+template<>
+void object::test<6>
+()
+{
+    input_ = GEOSGeom_createPointFromXY(std::numeric_limits<double>::quiet_NaN(), 1);
+    double d;
+    ensure_equals(GEOSMinimumClearance(input_, &d), 2);
+}
+
 } // namespace tut

commit cbf94fef29d4c485df73a66bf4cae89d5a24b402
Author: Martin Davis <mtnclimb at gmail.com>
Date:   Tue Apr 23 09:09:51 2024 -0700

    Update NEWS

diff --git a/NEWS.md b/NEWS.md
index d5a786315..b378d9fca 100644
--- a/NEWS.md
+++ b/NEWS.md
@@ -26,6 +26,7 @@
   - Fix PreparedPolygonContains for GC with MultiPoint (GH-1008, Martin Davis)
   - Fix reading WKT with EMPTY token with white space (GH-1025, Mike Taves)
   - Fix buffer Inverted Ring Removal check (GH-1056, Martin Davis)
+  - Add PointLocation.isOnSegment and remove LineIntersector point methods (GH-1083, Martin Davis)
 
 ## Changes in 3.12.0
 2023-06-27

commit d4b654ac9d3b2507c81153cf54f7b0e5c3ff9833
Author: Martin Davis <mtnclimb at gmail.com>
Date:   Tue Apr 23 09:08:02 2024 -0700

    Simplify code for PointLocation.isOnSegment

diff --git a/src/algorithm/PointLocation.cpp b/src/algorithm/PointLocation.cpp
index 5ad430045..4c8450e53 100644
--- a/src/algorithm/PointLocation.cpp
+++ b/src/algorithm/PointLocation.cpp
@@ -55,13 +55,12 @@ PointLocation::isOnLine(const geom::CoordinateXY& p, const geom::CoordinateSeque
         return false;
     }
 
-    const geom::CoordinateXY* pp = &(pt->getAt<geom::CoordinateXY>(0));
     for(std::size_t i = 1; i < ptsize; ++i) {
-        const geom::CoordinateXY& p1 = pt->getAt<geom::CoordinateXY>(i);
-        if(isOnSegment(p, *pp, p1)) {
+        if(isOnSegment(p, 
+                        pt->getAt<geom::CoordinateXY>(i - 1), 
+                        pt->getAt<geom::CoordinateXY>(i))) {
             return true;
         }
-        pp = &p1;
     }
     return false;
 }

commit 11fae139b1641d6514bd56c7f216683056322317
Author: Martin Davis <mtnclimb at gmail.com>
Date:   Tue Apr 23 08:49:19 2024 -0700

    Add PointLocation.isOnSegment (#1083)

diff --git a/include/geos/algorithm/LineIntersector.h b/include/geos/algorithm/LineIntersector.h
index e4a5c010d..958dbbf12 100644
--- a/include/geos/algorithm/LineIntersector.h
+++ b/include/geos/algorithm/LineIntersector.h
@@ -134,17 +134,6 @@ public:
         precisionModel = newPM;
     }
 
-    /// Compute the intersection of a point p and the line p1-p2.
-    ///
-    /// This function computes the boolean value of the hasIntersection test.
-    /// The actual value of the intersection (if there is one)
-    /// is equal to the value of <code>p</code>.
-    ///
-    void computeIntersection(const geom::CoordinateXY& p, const geom::CoordinateXY& p1, const geom::CoordinateXY& p2);
-
-    /// Same as above but doesn't compute intersection point. Faster.
-    static bool hasIntersection(const geom::CoordinateXY& p, const geom::CoordinateXY& p1, const geom::CoordinateXY& p2);
-
     enum intersection_type : uint8_t {
         /// Indicates that line segments do not intersect
         NO_INTERSECTION = 0,
diff --git a/include/geos/algorithm/PointLocation.h b/include/geos/algorithm/PointLocation.h
index b68010df6..e7c7f6acf 100644
--- a/include/geos/algorithm/PointLocation.h
+++ b/include/geos/algorithm/PointLocation.h
@@ -36,6 +36,16 @@ namespace algorithm { // geos::algorithm
 class GEOS_DLL PointLocation {
 public:
 
+    /** \brief
+     * Tests whether a point lies on a line segment.
+     * 
+     * @param p the point to test
+     * @param p0 a point of the line segment
+     * @param p1 a point of the line segment
+     * @return true if the point lies on the line segment
+     */
+    static bool isOnSegment(const geom::CoordinateXY& p, const geom::CoordinateXY& p0, const geom::CoordinateXY& p1);
+
     /** \brief
      * Tests whether a point lies on the line defined by a
      * [CoordinateSequence](@ref geom::CoordinateSequence).
diff --git a/src/algorithm/LineIntersector.cpp b/src/algorithm/LineIntersector.cpp
index 61ae787d5..b3901b9b3 100644
--- a/src/algorithm/LineIntersector.cpp
+++ b/src/algorithm/LineIntersector.cpp
@@ -212,40 +212,6 @@ LineIntersector::computeIntersection(const CoordinateSequence& p, std::size_t p0
     CoordinateSequences::binaryDispatch(p, q, dis);
 }
 
-/*public*/
-void
-LineIntersector::computeIntersection(const CoordinateXY& p, const CoordinateXY& p1, const CoordinateXY& p2)
-{
-    isProperVar = false;
-
-    // do between check first, since it is faster than the orientation test
-    if(Envelope::intersects(p1, p2, p)) {
-        if((Orientation::index(p1, p2, p) == 0) &&
-                (Orientation::index(p2, p1, p) == 0)) {
-            isProperVar = true;
-            if((p == p1) || (p == p2)) { // 2d only test
-                isProperVar = false;
-            }
-            result = POINT_INTERSECTION;
-            return;
-        }
-    }
-    result = NO_INTERSECTION;
-}
-
-/* public static */
-bool
-LineIntersector::hasIntersection(const CoordinateXY& p, const CoordinateXY& p1, const CoordinateXY& p2)
-{
-    if(Envelope::intersects(p1, p2, p)) {
-        if((Orientation::index(p1, p2, p) == 0) &&
-                (Orientation::index(p2, p1, p) == 0)) {
-            return true;
-        }
-    }
-    return false;
-}
-
 /* private static */
 const CoordinateXY&
 LineIntersector::nearestEndpoint(const CoordinateXY& p1, const CoordinateXY& p2,
diff --git a/src/algorithm/PointLocation.cpp b/src/algorithm/PointLocation.cpp
index 5ceb6bd53..5ad430045 100644
--- a/src/algorithm/PointLocation.cpp
+++ b/src/algorithm/PointLocation.cpp
@@ -20,16 +20,32 @@
 #include <vector>
 
 #include <geos/algorithm/LineIntersector.h>
+#include <geos/algorithm/Orientation.h>
 #include <geos/algorithm/PointLocation.h>
 #include <geos/algorithm/RayCrossingCounter.h>
 #include <geos/geom/CoordinateSequence.h>
 #include <geos/geom/Coordinate.h>
+#include <geos/geom/Envelope.h>
 #include <geos/geom/Location.h>
 #include <geos/util/IllegalArgumentException.h>
 
 namespace geos {
 namespace algorithm { // geos.algorithm
 
+/* public static */
+bool
+PointLocation::isOnSegment(const geom::CoordinateXY& p, const geom::CoordinateXY& p0, const geom::CoordinateXY& p1) 
+{
+    //-- test envelope first since it's faster
+    if (! geom::Envelope::intersects(p0, p1, p))
+        return false;
+    //-- handle zero-length segments
+    if (p.equals2D(p0))
+        return true;
+    bool isOnLine = Orientation::COLLINEAR == Orientation::index(p0, p1, p);
+    return isOnLine;
+}
+
 /* public static */
 bool
 PointLocation::isOnLine(const geom::CoordinateXY& p, const geom::CoordinateSequence* pt)
@@ -42,7 +58,7 @@ PointLocation::isOnLine(const geom::CoordinateXY& p, const geom::CoordinateSeque
     const geom::CoordinateXY* pp = &(pt->getAt<geom::CoordinateXY>(0));
     for(std::size_t i = 1; i < ptsize; ++i) {
         const geom::CoordinateXY& p1 = pt->getAt<geom::CoordinateXY>(i);
-        if(LineIntersector::hasIntersection(p, *pp, p1)) {
+        if(isOnSegment(p, *pp, p1)) {
             return true;
         }
         pp = &p1;
diff --git a/src/operation/valid/PolygonTopologyAnalyzer.cpp b/src/operation/valid/PolygonTopologyAnalyzer.cpp
index 39b672b1d..c2b9d0a39 100644
--- a/src/operation/valid/PolygonTopologyAnalyzer.cpp
+++ b/src/operation/valid/PolygonTopologyAnalyzer.cpp
@@ -180,8 +180,7 @@ PolygonTopologyAnalyzer::intersectingSegIndex(const CoordinateSequence* ringPts,
 {
     algorithm::LineIntersector li;
     for (std::size_t i = 0; i < ringPts->size() - 1; i++) {
-      li.computeIntersection(*pt, ringPts->getAt<CoordinateXY>(i), ringPts->getAt<CoordinateXY>(i+1));
-      if (li.hasIntersection()) {
+      if ( algorithm::PointLocation::isOnSegment(*pt, ringPts->getAt<CoordinateXY>(i), ringPts->getAt<CoordinateXY>(i+1)) ) {
         //-- check if pt is the start point of the next segment
         if (pt->equals2D(ringPts->getAt<CoordinateXY>(i + 1))) {
           return i + 1;
diff --git a/tests/unit/algorithm/PointLocationTest.cpp b/tests/unit/algorithm/PointLocationTest.cpp
new file mode 100644
index 000000000..bed1a1b0b
--- /dev/null
+++ b/tests/unit/algorithm/PointLocationTest.cpp
@@ -0,0 +1,129 @@
+//
+// Test Suite for geos::algorithm::PointLocation
+// Ported from JTS junit/algorithm/PointLocationTest.java
+
+#include <tut/tut.hpp>
+#include <utility.h>
+
+// geos
+#include <geos/geom/Coordinate.h>
+#include <geos/algorithm/PointLocation.h>
+
+// std
+#include <sstream>
+#include <string>
+#include <memory>
+
+using geos::algorithm::PointLocation;
+using geos::geom::CoordinateXY;
+
+namespace tut {
+//
+// Test Group
+//
+
+// dummy data, not used
+struct test_PointLocation_data {
+
+    geos::io::WKTReader r_;
+
+    void
+    checkOnLine(double x, double y, const std::string& wktLine, bool isExpected)
+    {
+        CoordinateXY p(x, y);
+        std::unique_ptr<CoordinateSequence> line = readPts(wktLine);
+        bool isOnLine = PointLocation::isOnLine(p, line.get());
+        ensure(isOnLine == isExpected);
+    }
+
+    void
+    checkOnSegment(double x, double y, const std::string& wktLine, bool isExpected)
+    {
+        CoordinateXY p(x, y);
+        std::unique_ptr<CoordinateSequence> line = readPts(wktLine);
+
+        bool isOnSeg = PointLocation::isOnSegment(p, line->getAt(0), line->getAt(1));
+        ensure(isOnSeg == isExpected);
+    }
+
+    std::unique_ptr<CoordinateSequence>
+    readPts(const std::string& wkt)
+    {
+        std::unique_ptr<Geometry> geom = r_.read(wkt);
+        const LineString* line = dynamic_cast<LineString*>(geom.get());
+        if (line)
+            return line->getCoordinatesRO()->clone();
+        else
+            return nullptr;
+    }
+
+};
+
+
+typedef test_group<test_PointLocation_data> group;
+typedef group::object object;
+
+group test_PointLocation_data("geos::algorithm::PointLocation");
+
+
+//
+// Test Cases
+//
+
+// testOnLineOnVertex
+template<>
+template<>
+void object::test<1> ()
+{
+    checkOnLine(20, 20, "LINESTRING (0 00, 20 20, 30 30)", true);
+}
+
+// testOnLineInSegment
+template<>
+template<>
+void object::test<2> ()
+{
+    checkOnLine(10, 10, "LINESTRING (0 0, 20 20, 0 40)", true);
+    checkOnLine(10, 30, "LINESTRING (0 0, 20 20, 0 40)", true);
+}
+
+// testNotOnLine
+template<>
+template<>
+void object::test<3> ()
+{
+    checkOnLine(0, 100, "LINESTRING (10 10, 20 10, 30 10)", false);
+}
+
+// testOnSegment
+template<>
+template<>
+void object::test<4> ()
+{
+    checkOnSegment(5, 5, "LINESTRING(0 0, 9 9)", true);
+    checkOnSegment(0, 0, "LINESTRING(0 0, 9 9)", true);
+    checkOnSegment(9, 9, "LINESTRING(0 0, 9 9)", true);
+}
+
+// testNotOnSegment
+template<>
+template<>
+void object::test<5> ()
+{
+    checkOnSegment(5, 6, "LINESTRING(0 0, 9 9)", false);
+    checkOnSegment(10, 10, "LINESTRING(0 0, 9 9)", false);
+    checkOnSegment(9, 9.00001, "LINESTRING(0 0, 9 9)", false);
+}
+
+// testOnZeroLengthSegment
+template<>
+template<>
+void object::test<6> ()
+{
+    checkOnSegment(1, 1, "LINESTRING(1 1, 1 1)", true);
+    checkOnSegment(1, 2, "LINESTRING(1 1, 1 1)", false);
+}
+
+
+} // namespace tut
+

commit d2c196b91d26267135c69165aab0f52adc26d10e
Author: Dan Baston <dbaston at gmail.com>
Date:   Sat Apr 20 17:55:12 2024 -0400

    Centroid: Avoid crash with empty hole (#1075)
    
    Resolves https://github.com/libgeos/geos/issues/1073

diff --git a/src/algorithm/Centroid.cpp b/src/algorithm/Centroid.cpp
index fa9355421..168a0996e 100644
--- a/src/algorithm/Centroid.cpp
+++ b/src/algorithm/Centroid.cpp
@@ -124,6 +124,10 @@ Centroid::addShell(const CoordinateSequence& pts)
 void
 Centroid::addHole(const CoordinateSequence& pts)
 {
+    if (pts.isEmpty()) {
+        return;
+    }
+
     bool isPositiveArea = Orientation::isCCW(&pts);
     for(std::size_t i = 0, e = pts.size() - 1; i < e; ++i) {
         addTriangle(*areaBasePt, pts.getAt<CoordinateXY>(i), pts.getAt<CoordinateXY>(i + 1), isPositiveArea);
diff --git a/tests/unit/algorithm/CentroidTest.cpp b/tests/unit/algorithm/CentroidTest.cpp
index 40ec77b55..152d12dff 100644
--- a/tests/unit/algorithm/CentroidTest.cpp
+++ b/tests/unit/algorithm/CentroidTest.cpp
@@ -122,5 +122,11 @@ void object::test<5>() {
         "POLYGON EMPTY");
 }
 
+template<>
+template<>
+void object::test<6>() {
+    checkCentroid("POLYGON ((0 0, 1 0, 1 1, 0 1, 0 0), EMPTY)", 0.5, 0.5);
+}
+
 } // namespace tut
 

commit c8b889be9e8fa22de8a34bea50fec3bb073f6898
Author: Even Rouault <even.rouault at spatialys.com>
Date:   Fri Apr 19 20:37:43 2024 +0200

    CMakeLists.txt: add compatibility with latest Intel Compiler (#1057)
    
    With those tunings (mostly forcing -fno-fast-math), build passes
    without warnings with ICC 2024.0.2.29 in Release mode, and tests pass as well.

diff --git a/CMakeLists.txt b/CMakeLists.txt
index 0538293ca..622838340 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -266,6 +266,38 @@ target_compile_definitions(geos_cxx_flags
   INTERFACE
     USE_UNSTABLE_GEOS_CPP_API)
 
+# Deal with Intel Compiler
+if (CMAKE_CXX_COMPILER_ID STREQUAL "IntelLLVM")
+  include(CheckCXXCompilerFlag)
+  check_cxx_compiler_flag(-fno-fast-math HAVE_FLAG_NO_FAST_MATH)
+  if (HAVE_FLAG_NO_FAST_MATH)
+    # Intel CXX compiler, based on clang, defaults to -ffast-math, which breaks a lot of things
+    target_compile_options(geos_cxx_flags INTERFACE "-fno-fast-math")
+  endif ()
+
+  set(TEST_LINK_STDCPP_SOURCE_CODE
+      "#include <string>
+    int main(){
+      std::string s;
+      s += \"x\";
+      return 0;
+    }")
+  check_cxx_source_compiles("${TEST_LINK_STDCPP_SOURCE_CODE}" _TEST_LINK_STDCPP)
+  if( NOT _TEST_LINK_STDCPP )
+      message(WARNING "Cannot link code using standard C++ library. Automatically adding -lstdc++ to CMAKE_EXE_LINKER_FLAGS, CMAKE_SHARED_LINKER_FLAGS and CMAKE_MODULE_LINKER_FLAGS")
+      set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} -lstdc++")
+      set(CMAKE_SHARED_LINKER_FLAGS "${CMAKE_SHARED_LINKER_FLAGS} -lstdc++")
+      set(CMAKE_MODULE_LINKER_FLAGS "${CMAKE_MODULE_LINKER_FLAGS} -lstdc++")
+
+      include(CheckCXXSourceCompiles)
+      check_cxx_source_compiles("${TEST_LINK_STDCPP_SOURCE_CODE}" _TEST_LINK_STDCPP_AGAIN)
+      if( NOT _TEST_LINK_STDCPP_AGAIN )
+          message(FATAL_ERROR "Cannot link C++ program")
+      endif()
+  endif()
+endif ()
+
+
 target_compile_definitions(geos_developer_cxx_flags
   INTERFACE
     $<$<BOOL:${MSVC}>:_CRT_NONSTDC_NO_DEPRECATE>

commit 9a5c9a5ae0181baf259da460572301d60fbfb193
Author: Daniel Baston <dbaston at gmail.com>
Date:   Mon Apr 15 14:45:37 2024 -0400

    Reformat files for curved geometry classes

diff --git a/include/geos/algorithm/ConvexHull.h b/include/geos/algorithm/ConvexHull.h
index df0f69ab6..bed2e0f3a 100644
--- a/include/geos/algorithm/ConvexHull.h
+++ b/include/geos/algorithm/ConvexHull.h
@@ -181,7 +181,7 @@ public:
         : inputGeom(newGeometry)
         , geomFactory(newGeometry->getFactory())
     {
-      util::ensureNotCurvedType(inputGeom);
+      util::ensureNoCurvedComponents(inputGeom);
     };
 
     ~ConvexHull() {};
diff --git a/include/geos/geom/CircularString.h b/include/geos/geom/CircularString.h
index 7d34cd9b2..283cb636c 100644
--- a/include/geos/geom/CircularString.h
+++ b/include/geos/geom/CircularString.h
@@ -35,40 +35,49 @@ public:
 
     GeometryTypeId getGeometryTypeId() const override;
 
-    CircularString* cloneImpl() const override
+    double getLength() const override
     {
-        return new CircularString(*this);
-    }
-
-    CircularString* reverseImpl() const override;
-
-    int
-    getSortIndex() const override
-    {
-        return SORTINDEX_LINESTRING;
-    };
-
-    double getLength() const override {
         throw util::UnsupportedOperationException("Cannot calculate length of CircularString");
     }
 
-    bool hasCurvedComponents() const override {
+    bool hasCurvedComponents() const override
+    {
         return true;
     }
 
+    std::unique_ptr<CircularString> reverse() const
+    {
+        return std::unique_ptr<CircularString>(reverseImpl());
+    }
+
 protected:
 
     /// \brief
     /// Constructs a CircularString taking ownership the
     /// given CoordinateSequence.
-    CircularString(std::unique_ptr<CoordinateSequence> && pts,
+    CircularString(std::unique_ptr<CoordinateSequence>&& pts,
                    const GeometryFactory& newFactory);
 
+    CircularString* cloneImpl() const override
+    {
+        return new CircularString(*this);
+    }
+
     void geometryChangedAction() override
     {
         envelope = computeEnvelopeInternal(false);
     }
 
+    int
+    getSortIndex() const override
+    {
+        return SORTINDEX_LINESTRING;
+    };
+
+    CircularString* reverseImpl() const override;
+
+    void validateConstruction();
+
 };
 
 
diff --git a/include/geos/geom/CompoundCurve.h b/include/geos/geom/CompoundCurve.h
index 253e7dce2..7d82b8aa7 100644
--- a/include/geos/geom/CompoundCurve.h
+++ b/include/geos/geom/CompoundCurve.h
@@ -24,91 +24,93 @@ namespace geom {
 class GEOS_DLL CompoundCurve : public Curve {
     friend class GeometryFactory;
 
-
 public:
+    using Curve::apply_ro;
+    using Curve::apply_rw;
 
-    CompoundCurve(const CompoundCurve&);
+    void apply_ro(CoordinateFilter* filter) const override;
 
-    CompoundCurve& operator=(const CompoundCurve&);
+    void apply_ro(CoordinateSequenceFilter& filter) const override;
 
-    std::unique_ptr<CoordinateSequence> getCoordinates() const override;
+    void apply_rw(CoordinateSequenceFilter& filter) override;
 
-    const CoordinateXY* getCoordinate() const override;
+    void apply_rw(const CoordinateFilter* filter) override;
 
-    uint8_t getCoordinateDimension() const override;
+    int compareToSameClass(const Geometry* geom) const override;
 
-    bool hasZ() const override;
-
-    bool hasM() const override;
-
-    bool isEmpty() const override;
-
-    bool isClosed() const override;
-
-    std::size_t getNumCurves() const;
-
-    const SimpleCurve* getCurveN(std::size_t) const;
-
-    std::size_t getNumPoints() const override;
-
-    std::unique_ptr<Geometry> getBoundary() const override;
-
-    std::string getGeometryType() const override;
-
-    GeometryTypeId getGeometryTypeId() const override;
+    std::unique_ptr<CompoundCurve> clone() const;
 
     bool equalsExact(const Geometry* other, double tolerance = 0)
     const override;
 
     bool equalsIdentical(const Geometry* other) const override;
 
+    std::unique_ptr<Geometry> getBoundary() const override;
+
+    const CoordinateXY* getCoordinate() const override;
+
+    uint8_t getCoordinateDimension() const override;
+
+    std::unique_ptr<CoordinateSequence> getCoordinates() const override;
+
+    /// Returns the nth section of the CompoundCurve
+    const SimpleCurve* getCurveN(std::size_t) const;
+
     const Envelope* getEnvelopeInternal() const override
     {
         return &envelope;
     }
 
-    std::unique_ptr<CompoundCurve> clone() const;
+    std::string getGeometryType() const override;
 
-    CompoundCurve* cloneImpl() const override;
-
-    std::unique_ptr<CompoundCurve> reverse() const;
-
-    CompoundCurve* reverseImpl() const override;
+    GeometryTypeId getGeometryTypeId() const override;
 
     double getLength() const override;
 
-    using Curve::apply_ro;
-    using Curve::apply_rw;
+    /// Returns the number of sections in the CompoundCurve
+    std::size_t getNumCurves() const;
 
-    void apply_rw(const CoordinateFilter* filter) override;
-
-    void apply_ro(CoordinateFilter* filter) const override;
-
-    void apply_rw(CoordinateSequenceFilter& filter) override;
-
-    void apply_ro(CoordinateSequenceFilter& filter) const override;
-
-    void normalize() override;
-
-    int compareToSameClass(const Geometry* geom) const override;
+    std::size_t getNumPoints() const override;
 
     bool hasCurvedComponents() const override;
 
+    bool hasM() const override;
+
+    bool hasZ() const override;
+
+    bool isClosed() const override;
+
+    bool isEmpty() const override;
+
+    void normalize() override;
+
+    std::unique_ptr<CompoundCurve> reverse() const;
+
 protected:
+    /// Construct a CompoundCurve, taking ownership of the
+    /// provided CoordinateSequence
     CompoundCurve(std::vector<std::unique_ptr<SimpleCurve>>&&,
                   const GeometryFactory&);
 
-    int getSortIndex() const override
-    {
-        return SORTINDEX_COMPOUNDCURVE;
-    }
+    CompoundCurve(const CompoundCurve&);
+
+    CompoundCurve& operator=(const CompoundCurve&);
+
+    CompoundCurve* cloneImpl() const override;
+
+    Envelope computeEnvelopeInternal() const;
 
     void geometryChangedAction() override
     {
         envelope = computeEnvelopeInternal();
     }
 
-    Envelope computeEnvelopeInternal() const;
+    int getSortIndex() const override
+    {
+        return SORTINDEX_COMPOUNDCURVE;
+    }
+
+    CompoundCurve* reverseImpl() const override;
 
 private:
     std::vector<std::unique_ptr<SimpleCurve>> curves;
diff --git a/include/geos/geom/Curve.h b/include/geos/geom/Curve.h
index a8367c817..9de1a627a 100644
--- a/include/geos/geom/Curve.h
+++ b/include/geos/geom/Curve.h
@@ -22,35 +22,41 @@ namespace geom {
 class GEOS_DLL Curve : public Geometry {
 
 public:
-    /// Returns line dimension (1)
-    Dimension::DimensionType getDimension() const override
-    {
-        return Dimension::L; // line
-    }
+    using Geometry::apply_ro;
+    using Geometry::apply_rw;
 
+    void apply_ro(GeometryComponentFilter* filter) const override;
+
+    void apply_ro(GeometryFilter* filter) const override;
+
+    void apply_rw(GeometryComponentFilter* filter) override;
+
+    void apply_rw(GeometryFilter* filter) override;
+
+    /**
+     * \brief
+     * Returns Dimension::False for a closed Curve,
+     * 0 otherwise (Curve boundary is a MultiPoint)
+     */
     int
     getBoundaryDimension() const override
     {
         return isClosed() ? Dimension::False : 0;
     }
 
+    /// Returns line dimension (1)
+    Dimension::DimensionType getDimension() const override
+    {
+        return Dimension::L; // line
+    }
+
+    /// Returns true if the first and last coordinate in the Curve are the same
     virtual bool isClosed() const = 0;
 
+    /// Returns true if the curve is closed and simple
     bool isRing() const;
 
-    using Geometry::apply_ro;
-    using Geometry::apply_rw;
-
-    void apply_rw(GeometryFilter* filter) override;
-
-    void apply_ro(GeometryFilter* filter) const override;
-
-    void apply_rw(GeometryComponentFilter* filter) override;
-
-    void apply_ro(GeometryComponentFilter* filter) const override;
-
 protected:
-
     Curve(const GeometryFactory& factory) : Geometry(&factory) {}
 
 };
diff --git a/include/geos/geom/CurvePolygon.h b/include/geos/geom/CurvePolygon.h
index a030fc66b..1a12b4837 100644
--- a/include/geos/geom/CurvePolygon.h
+++ b/include/geos/geom/CurvePolygon.h
@@ -25,31 +25,31 @@ class GEOS_DLL CurvePolygon : public SurfaceImpl<Curve> {
 public:
     ~CurvePolygon() override = default;
 
+    double getArea() const override;
+
+    std::unique_ptr<Geometry> getBoundary() const override;
+
     std::unique_ptr<CoordinateSequence> getCoordinates() const override;
 
+    std::string getGeometryType() const override;
+
+    GeometryTypeId getGeometryTypeId() const override;
+
+    bool hasCurvedComponents() const override;
+
+    void normalize() override;
+
+protected:
+    using SurfaceImpl::SurfaceImpl;
+
+    Geometry* cloneImpl() const override;
+
     int
     getSortIndex() const override
     {
         return SORTINDEX_CURVEPOLYGON;
     }
 
-    std::string getGeometryType() const override;
-
-    GeometryTypeId getGeometryTypeId() const override;
-
-    std::unique_ptr<Geometry> getBoundary() const override;
-
-    void normalize() override;
-
-    double getArea() const override;
-
-    bool hasCurvedComponents() const override;
-
-protected:
-    using SurfaceImpl::SurfaceImpl;
-
-    Geometry* cloneImpl() const override;
-
     Geometry* reverseImpl() const override;
 };
 
diff --git a/include/geos/geom/Geometry.h b/include/geos/geom/Geometry.h
index ead07c1b9..342d0cf6b 100644
--- a/include/geos/geom/Geometry.h
+++ b/include/geos/geom/Geometry.h
@@ -309,12 +309,6 @@ public:
     /// Return a string representation of this Geometry type
     virtual std::string getGeometryType() const = 0; //Abstract
 
-    /// Returns whether the Geometry type _may_ contain curved elements
-    /// FIXME: this would be true for GeometryCollection ?
-    bool isCurvedType() const;
-
-    static bool isCurvedType(GeometryTypeId);
-
     /// Returns whether the Geometry contains curved components
     virtual bool hasCurvedComponents() const;
 
@@ -946,21 +940,25 @@ protected:
         while(i < a.size() && j < b.size()) {
             const auto& aGeom = *a[i];
             const auto& bGeom = *b[j];
-        int comparison = aGeom.compareTo(&bGeom);
-        if(comparison != 0) {
-            return comparison;
-        }
-        i++;
-        j++;
-        }
-        if(i < a.size()) {
-        return 1;
-        }
-        if(j < b.size()) {
-        return -1;
-        }
-        return 0;
 
+            int comparison = aGeom.compareTo(&bGeom);
+            if(comparison != 0) {
+                return comparison;
+            }
+
+            i++;
+            j++;
+        }
+
+        if(i < a.size()) {
+            return 1;
+        }
+
+        if(j < b.size()) {
+            return -1;
+        }
+
+        return 0;
     }
 
     bool equal(const CoordinateXY& a, const CoordinateXY& b,
diff --git a/include/geos/geom/GeometryFactory.h b/include/geos/geom/GeometryFactory.h
index e0da35330..5737cdd81 100644
--- a/include/geos/geom/GeometryFactory.h
+++ b/include/geos/geom/GeometryFactory.h
@@ -325,8 +325,10 @@ public:
     std::unique_ptr<CircularString> createCircularString(
         const CoordinateSequence& coordinates) const;
 
+    /// Construct an EMPTY CompoundCurve
     std::unique_ptr<CompoundCurve> createCompoundCurve() const;
 
+    /// Construct a CompoundCurve taking ownership of given argument
     std::unique_ptr<CompoundCurve> createCompoundCurve(std::vector<std::unique_ptr<SimpleCurve>>&&) const;
 
     /**
diff --git a/include/geos/geom/MultiCurve.h b/include/geos/geom/MultiCurve.h
index f626bcdf0..5505f5d96 100644
--- a/include/geos/geom/MultiCurve.h
+++ b/include/geos/geom/MultiCurve.h
@@ -26,18 +26,13 @@ class GEOS_DLL MultiCurve : public GeometryCollection {
 public:
     ~MultiCurve() override = default;
 
-    /// Returns line dimension (1)
-    Dimension::DimensionType getDimension() const override;
-
-    bool hasDimension(Dimension::DimensionType d) const override
+    std::unique_ptr<MultiCurve> clone() const
     {
-        return d == Dimension::L;
-    }
+        return std::unique_ptr<MultiCurve>(cloneImpl());
+    };
 
-    bool isDimensionStrict(Dimension::DimensionType d) const override
-    {
-        return d == Dimension::L;
-    }
+    /// Returns a (possibly empty) [MultiPoint](@ref geom::MultiPoint)
+    std::unique_ptr<Geometry> getBoundary() const override;
 
     /**
      * \brief
@@ -46,8 +41,8 @@ public:
      */
     int getBoundaryDimension() const override;
 
-    /// Returns a (possibly empty) [MultiPoint](@ref geom::MultiPoint)
-    std::unique_ptr<Geometry> getBoundary() const override;
+    /// Returns line dimension (1)
+    Dimension::DimensionType getDimension() const override;
 
     const Curve* getGeometryN(std::size_t n) const override;
 
@@ -55,12 +50,19 @@ public:
 
     GeometryTypeId getGeometryTypeId() const override;
 
+    bool hasDimension(Dimension::DimensionType d) const override
+    {
+        return d == Dimension::L;
+    }
+
+    /// Returns true if the MultiCurve is not empty, and every included
+    /// Curve is also closed.
     bool isClosed() const;
 
-    std::unique_ptr<MultiCurve> clone() const
+    bool isDimensionStrict(Dimension::DimensionType d) const override
     {
-        return std::unique_ptr<MultiCurve>(cloneImpl());
-    };
+        return d == Dimension::L;
+    }
 
     /**
      * Creates a MultiCurve in the reverse
diff --git a/include/geos/geom/MultiSurface.h b/include/geos/geom/MultiSurface.h
index af47a947e..73871e4f0 100644
--- a/include/geos/geom/MultiSurface.h
+++ b/include/geos/geom/MultiSurface.h
@@ -26,9 +26,29 @@ public:
 
     ~MultiSurface() override;
 
+    std::unique_ptr<MultiSurface> clone() const
+    {
+        return std::unique_ptr<MultiSurface>(cloneImpl());
+    };
+
+    /** \brief
+     * Computes the boundary of this geometry
+     *
+     * @return a lineal geometry (which may be empty)
+     * @see Geometry#getBoundary
+     */
+    std::unique_ptr<Geometry> getBoundary() const override;
+
+    /// Returns 1 (MultiSurface boundary is MultiCurve)
+    int getBoundaryDimension() const override;
+
     /// Returns surface dimension (2)
     Dimension::DimensionType getDimension() const override;
 
+    std::string getGeometryType() const override;
+
+    GeometryTypeId getGeometryTypeId() const override;
+
     bool hasDimension(Dimension::DimensionType d) const override
     {
         return d == Dimension::A;
@@ -39,26 +59,6 @@ public:
         return d == Dimension::A;
     }
 
-    /// Returns 1 (MultiSurface boundary is MultiCurve)
-    int getBoundaryDimension() const override;
-
-    /** \brief
-     * Computes the boundary of this geometry
-     *
-     * @return a lineal geometry (which may be empty)
-     * @see Geometry#getBoundary
-     */
-    std::unique_ptr<Geometry> getBoundary() const override;
-
-    std::string getGeometryType() const override;
-
-    GeometryTypeId getGeometryTypeId() const override;
-
-    std::unique_ptr<MultiSurface> clone() const
-    {
-        return std::unique_ptr<MultiSurface>(cloneImpl());
-    };
-
     std::unique_ptr<MultiSurface> reverse() const
     {
         return std::unique_ptr<MultiSurface>(reverseImpl());
@@ -81,14 +81,14 @@ protected:
         return new MultiSurface(*this);
     }
 
-    MultiSurface* reverseImpl() const override;
-
     int
     getSortIndex() const override
     {
         return SORTINDEX_MULTISURFACE;
     };
 
+    MultiSurface* reverseImpl() const override;
+
 };
 }
 }
diff --git a/include/geos/geom/SimpleCurve.h b/include/geos/geom/SimpleCurve.h
index 187af0c91..2d7037a0d 100644
--- a/include/geos/geom/SimpleCurve.h
+++ b/include/geos/geom/SimpleCurve.h
@@ -26,44 +26,51 @@ namespace geom {
 class GEOS_DLL SimpleCurve : public Curve {
 public:
 
+    using Curve::apply_ro;
+    using Curve::apply_rw;
+
+    void apply_ro(CoordinateFilter* filter) const override;
+
+    void apply_ro(CoordinateSequenceFilter& filter) const override;
+
+    void apply_rw(CoordinateSequenceFilter& filter) override;
+
+    void apply_rw(const CoordinateFilter* filter) override;
+
+    bool equalsExact(const Geometry* other, double tolerance = 0)
+    const override;
+
+    bool equalsIdentical(const Geometry* other) const override;
+
+    /**
+     * \brief
+     * Returns a MultiPoint.
+     * Empty for closed Curve, a Point for each vertex otherwise.
+     */
+    std::unique_ptr<Geometry> getBoundary() const override;
+
+    const CoordinateXY* getCoordinate() const override;
+
+    /// Returns coordinate dimension.
+    uint8_t getCoordinateDimension() const override;
+
+    virtual const Coordinate& getCoordinateN(std::size_t n) const;
+
     std::unique_ptr<CoordinateSequence> getCoordinates() const override;
 
     /// Returns a read-only pointer to internal CoordinateSequence
     const CoordinateSequence* getCoordinatesRO() const;
 
-    virtual const Coordinate& getCoordinateN(std::size_t n) const;
+    /// \brief
+    /// Return the end point of the LineString
+    /// or NULL if this is an EMPTY LineString.
+    ///
+    virtual std::unique_ptr<Point> getEndPoint() const;
 
-    /**
-     * \brief
-     * Take ownership of the CoordinateSequence managed by this geometry.
-     * After releasing the coordinates, the geometry should be considered
-     * in a moved-from state and should not be accessed.
-     * @return this Geometry's CoordinateSequence.
-     */
-    std::unique_ptr<CoordinateSequence> releaseCoordinates();
-
-    /**
-     * \brief
-     * Returns Dimension::False for a closed LineString,
-     * 0 otherwise (LineString boundary is a MultiPoint)
-     */
-    int getBoundaryDimension() const override;
-
-    /// Returns coordinate dimension.
-    uint8_t getCoordinateDimension() const override;
-
-    bool hasM() const override;
-
-    bool hasZ() const override;
-
-    /**
-     * \brief
-     * Returns a MultiPoint.
-     * Empty for closed LineString, a Point for each vertex otherwise.
-     */
-    std::unique_ptr<Geometry> getBoundary() const override;
-
-    bool isEmpty() const override;
+    const Envelope* getEnvelopeInternal() const override
+    {
+        return &envelope;
+    }
 
     std::size_t getNumPoints() const override;
 
@@ -75,38 +82,15 @@ public:
     ///
     virtual std::unique_ptr<Point> getStartPoint() const;
 
-    /// \brief
-    /// Return the end point of the LineString
-    /// or NULL if this is an EMPTY LineString.
-    ///
-    virtual std::unique_ptr<Point> getEndPoint() const;
+    bool hasM() const override;
+
+    bool hasZ() const override;
 
     bool isClosed() const override;
 
     virtual bool isCoordinate(CoordinateXY& pt) const;
 
-    bool equalsExact(const Geometry* other, double tolerance = 0)
-    const override;
-
-    bool equalsIdentical(const Geometry* other) const override;
-
-    const CoordinateXY* getCoordinate() const override;
-
-    const Envelope* getEnvelopeInternal() const override
-    {
-        return &envelope;
-    }
-
-    using Curve::apply_ro;
-    using Curve::apply_rw;
-
-    void apply_rw(const CoordinateFilter* filter) override;
-
-    void apply_ro(CoordinateFilter* filter) const override;
-
-    void apply_rw(CoordinateSequenceFilter& filter) override;
-
-    void apply_ro(CoordinateSequenceFilter& filter) const override;
+    bool isEmpty() const override;
 
     /** \brief
      * Normalizes a SimpleCurve.
@@ -117,9 +101,14 @@ public:
      */
     void normalize() override;
 
-    //was protected
-    int compareToSameClass(const Geometry* ls) const override;
-
+    /**
+     * \brief
+     * Take ownership of the CoordinateSequence managed by this geometry.
+     * After releasing the coordinates, the geometry should be considered
+     * in a moved-from state and should not be accessed.
+     * @return this Geometry's CoordinateSequence.
+     */
+    std::unique_ptr<CoordinateSequence> releaseCoordinates();
 
 protected:
 
@@ -129,13 +118,15 @@ protected:
                 bool isLinear,
                 const GeometryFactory& factory);
 
+    int compareToSameClass(const Geometry* ls) const override;
+
     Envelope computeEnvelopeInternal(bool isLinear) const;
 
-    // TODO: hold value or shared_ptr instead of unique_ptr
+    // TODO: hold value or shared_ptr instead of unique_ptr?
     std::unique_ptr<CoordinateSequence> points;
-
     mutable Envelope envelope;
 
+
 private:
 
     void normalizeClosed();
diff --git a/include/geos/geom/Surface.h b/include/geos/geom/Surface.h
index 46b491748..7b1bb6ff3 100644
--- a/include/geos/geom/Surface.h
+++ b/include/geos/geom/Surface.h
@@ -21,6 +21,9 @@ namespace geom {
 
 class Curve;
 
+/// A Surface is an abstract class representing a Geometry of dimension 2.
+/// It is extended by Polygon, which represents a Surface with linear edges,
+/// and by CurvePolygon, whose edges may include circular arcs.
 class GEOS_DLL Surface : public Geometry {
 
 private:
@@ -30,19 +33,23 @@ protected:
 
 public:
 
-    uint8_t getCoordinateDimension() const override;
+    void apply_ro(CoordinateFilter* filter) const override;
 
-    /// Returns the exterior ring (shell)
-    virtual const Curve* getExteriorRing() const = 0;
+    void apply_ro(CoordinateSequenceFilter& filter) const override;
 
-    /// Returns number of interior rings (holes)
-    virtual size_t getNumInteriorRing() const = 0;
+    void apply_ro(GeometryComponentFilter* filter) const override;
 
-    /// Get nth interior ring (hole)
-    virtual const Curve* getInteriorRingN(std::size_t n) const = 0;
+    void apply_ro(GeometryFilter* filter) const override;
 
-    size_t getNumPoints() const override;
+    void apply_rw(CoordinateSequenceFilter& filter) override;
 
+    void apply_rw(GeometryComponentFilter* filter) override;
+
+    void apply_rw(GeometryFilter* filter) override;
+
+    void apply_rw(const CoordinateFilter* filter) override;
+
+    std::unique_ptr<Geometry> convexHull() const override;
 
     bool
     equalsExact(const Geometry* other, double tolerance = 0.0) const override;
@@ -50,9 +57,6 @@ public:
     bool
     equalsIdentical(const Geometry* other) const override;
 
-
-    std::unique_ptr<Geometry> convexHull() const override;
-
     int
     getBoundaryDimension() const override
     {
@@ -61,6 +65,8 @@ public:
 
     const CoordinateXY* getCoordinate() const override;
 
+    uint8_t getCoordinateDimension() const override;
+
     Dimension::DimensionType
     getDimension() const override
     {
@@ -69,52 +75,40 @@ public:
 
     const Envelope* getEnvelopeInternal() const override;
 
+    /// Returns the exterior ring (shell)
+    virtual const Curve* getExteriorRing() const = 0;
+
+    /// Get nth interior ring (hole)
+    virtual const Curve* getInteriorRingN(std::size_t n) const = 0;
+
+    /// Returns the perimeter of this Surface
+    double getLength() const override;
+
+    /// Returns number of interior rings (holes)
+    virtual size_t getNumInteriorRing() const = 0;
+
+    size_t getNumPoints() const override;
+
     bool hasM() const override;
 
     bool hasZ() const override;
 
     bool isEmpty() const override;
 
-    /// Returns the perimeter of this Surface
-    double getLength() const override;
-
-    void
-    apply_ro(CoordinateFilter* filter) const override;
-
-    void
-    apply_rw(const CoordinateFilter* filter) override;
-
-    void
-    apply_rw(GeometryFilter* filter) override;
-
-    void
-    apply_ro(GeometryFilter* filter) const override;
-
-    void
-    apply_ro(GeometryComponentFilter* filter) const override;
-
-    void
-    apply_rw(GeometryComponentFilter* filter) override;
-
-    void
-    apply_rw(CoordinateSequenceFilter& filter) override;
-
-    void
-    apply_ro(CoordinateSequenceFilter& filter) const override;
-
 protected:
 
     int
     compareToSameClass(const Geometry* g) const override;
 
-    void geometryChangedAction() override {}
-
+    // Helper method allowing PolygonImpl to use GeometryFactory without cirular imports
     static std::unique_ptr<Geometry> createEmptyRing(const GeometryFactory&);
 
     virtual Curve* getExteriorRing() = 0;
 
     virtual Curve* getInteriorRingN(std::size_t i) = 0;
 
+    void geometryChangedAction() override {}
+
 };
 
 }
diff --git a/include/geos/geom/SurfaceImpl.h b/include/geos/geom/SurfaceImpl.h
index a69ecd0cd..60da401ef 100644
--- a/include/geos/geom/SurfaceImpl.h
+++ b/include/geos/geom/SurfaceImpl.h
@@ -17,12 +17,12 @@
 
 #pragma once
 
-#include <geos/geom/Curve.h>
-#include <geos/geom/Surface.h>
 #include <geos/geom/CoordinateSequenceFilter.h>
+#include <geos/geom/Curve.h>
 #include <geos/geom/GeometryComponentFilter.h>
 #include <geos/geom/GeometryFilter.h>
 #include <geos/geom/LinearRing.h>
+#include <geos/geom/Surface.h>
 #include <geos/util/IllegalArgumentException.h>
 
 namespace geos {
@@ -93,7 +93,6 @@ protected:
 
 public:
 
-
     const RingType*
     getExteriorRing() const override
     {
@@ -106,6 +105,23 @@ public:
         return shell.get();
     }
 
+    const RingType*
+    getInteriorRingN(std::size_t n) const override
+    {
+        return holes[n].get();
+    }
+
+    RingType*
+    getInteriorRingN(std::size_t n) override
+    {
+        return holes[n].get();
+    }
+
+    size_t getNumInteriorRing() const override
+    {
+        return holes.size();
+    }
+
     /**
     * \brief
     * Take ownership of this Surface's exterior ring.
@@ -120,23 +136,6 @@ public:
         return std::move(shell);
     }
 
-    size_t getNumInteriorRing() const override
-    {
-        return holes.size();
-    }
-
-    const RingType*
-    getInteriorRingN(std::size_t n) const override
-    {
-        return holes[n].get();
-    }
-
-    RingType*
-    getInteriorRingN(std::size_t n) override
-    {
-        return holes[n].get();
-    }
-
     /**
     * \brief
     * Take ownership of this Surfaces's interior rings.
diff --git a/include/geos/io/WKTWriter.h b/include/geos/io/WKTWriter.h
index e58c01c9c..97007dc17 100644
--- a/include/geos/io/WKTWriter.h
+++ b/include/geos/io/WKTWriter.h
@@ -282,13 +282,6 @@ protected:
         OrdinateSet outputOrdinates,
         int level, bool doIndent, Writer& writer) const;
 
-#if 0
-    void appendLineStringText(
-        const geom::LineString& lineString,
-        OrdinateSet outputOrdinates,
-        int level, bool doIndent, Writer& writer) const;
-#endif
-
     void appendSurfaceText(
         const geom::Surface& polygon,
         OrdinateSet outputOrdinates,
diff --git a/include/geos/operation/union/UnaryUnionOp.h b/include/geos/operation/union/UnaryUnionOp.h
index 6c62127b9..3fed7e32b 100644
--- a/include/geos/operation/union/UnaryUnionOp.h
+++ b/include/geos/operation/union/UnaryUnionOp.h
@@ -171,7 +171,7 @@ private:
     void
     extract(const geom::Geometry& geom)
     {
-        util::ensureNotCurvedType(geom);
+        util::ensureNoCurvedComponents(geom);
 
         using namespace geom::util;
 
diff --git a/include/geos/util.h b/include/geos/util.h
index 04405b1e2..fe0825b85 100644
--- a/include/geos/util.h
+++ b/include/geos/util.h
@@ -64,21 +64,20 @@ template<typename To, typename From> inline To down_cast(From* f)
 namespace util {
 
 template<typename T>
-void ensureNotCurvedType(const T* geom)
-{
-    if (geom->hasCurvedComponents()) {
-        throw UnsupportedOperationException("Curved geometry types are not supported.");
-    }
-}
-
-template<typename T>
-void ensureNotCurvedType(const T& geom)
+void ensureNoCurvedComponents(const T& geom)
 {
     if (geom.hasCurvedComponents()) {
         throw UnsupportedOperationException("Curved geometry types are not supported.");
     }
 }
 
+template<typename T>
+void ensureNoCurvedComponents(const T* geom)
+{
+    ensureNoCurvedComponents(*geom);
+}
+
+
 }
 
 
diff --git a/src/algorithm/Centroid.cpp b/src/algorithm/Centroid.cpp
index c99588da8..313bc1b41 100644
--- a/src/algorithm/Centroid.cpp
+++ b/src/algorithm/Centroid.cpp
@@ -70,7 +70,7 @@ Centroid::getCentroid(CoordinateXY& cent) const
 void
 Centroid::add(const Geometry& geom)
 {
-    util::ensureNotCurvedType(geom);
+    util::ensureNoCurvedComponents(geom);
 
     if(geom.isEmpty()) {
         return;
diff --git a/src/algorithm/distance/DiscreteFrechetDistance.cpp b/src/algorithm/distance/DiscreteFrechetDistance.cpp
index 03934def7..99a650f19 100644
--- a/src/algorithm/distance/DiscreteFrechetDistance.cpp
+++ b/src/algorithm/distance/DiscreteFrechetDistance.cpp
@@ -144,8 +144,8 @@ DiscreteFrechetDistance::compute(
         throw util::IllegalArgumentException("DiscreteFrechetDistance called with empty inputs.");
     }
 
-    util::ensureNotCurvedType(discreteGeom);
-    util::ensureNotCurvedType(geom);
+    util::ensureNoCurvedComponents(discreteGeom);
+    util::ensureNoCurvedComponents(geom);
 
     auto lp = discreteGeom.getCoordinates();
     auto lq = geom.getCoordinates();
diff --git a/src/algorithm/distance/DiscreteHausdorffDistance.cpp b/src/algorithm/distance/DiscreteHausdorffDistance.cpp
index a02f0a661..3712b0a40 100644
--- a/src/algorithm/distance/DiscreteHausdorffDistance.cpp
+++ b/src/algorithm/distance/DiscreteHausdorffDistance.cpp
@@ -103,8 +103,8 @@ DiscreteHausdorffDistance::computeOrientedDistance(
     const geom::Geometry& geom,
     PointPairDistance& p_ptDist)
 {
-    util::ensureNotCurvedType(discreteGeom);
-    util::ensureNotCurvedType(geom);
+    util::ensureNoCurvedComponents(discreteGeom);
+    util::ensureNoCurvedComponents(geom);
 
     // can't calculate distance with empty
     if (discreteGeom.isEmpty() || geom.isEmpty()) return;
diff --git a/src/algorithm/hull/ConcaveHull.cpp b/src/algorithm/hull/ConcaveHull.cpp
index 927008862..d70a80d46 100644
--- a/src/algorithm/hull/ConcaveHull.cpp
+++ b/src/algorithm/hull/ConcaveHull.cpp
@@ -58,7 +58,7 @@ ConcaveHull::ConcaveHull(const Geometry* geom)
     , maxSizeInHull(0.0)
     , geomFactory(geom->getFactory())
 {
-    util::ensureNotCurvedType(geom);
+    util::ensureNoCurvedComponents(geom);
 }
 
 /* public static */
diff --git a/src/algorithm/hull/ConcaveHullOfPolygons.cpp b/src/algorithm/hull/ConcaveHullOfPolygons.cpp
index e26176254..d0ab8e426 100644
--- a/src/algorithm/hull/ConcaveHullOfPolygons.cpp
+++ b/src/algorithm/hull/ConcaveHullOfPolygons.cpp
@@ -113,7 +113,7 @@ ConcaveHullOfPolygons::ConcaveHullOfPolygons(const Geometry* geom)
     , isHolesAllowed(false)
     , isTight(false)
 {
-    util::ensureNotCurvedType(geom);
+    util::ensureNoCurvedComponents(geom);
     if (! geom->isPolygonal()) {
         throw util::IllegalArgumentException("Input must be polygonal");
     }
diff --git a/src/coverage/CoverageRingEdges.cpp b/src/coverage/CoverageRingEdges.cpp
index fde884f28..e6ec619b7 100644
--- a/src/coverage/CoverageRingEdges.cpp
+++ b/src/coverage/CoverageRingEdges.cpp
@@ -71,7 +71,7 @@ CoverageRingEdges::build()
     std::map<LineSegment, CoverageEdge*> uniqueEdgeMap;
     for (const Geometry* geom : m_coverage) {
         for (std::size_t ipoly = 0; ipoly < geom->getNumGeometries(); ipoly++) {
-            util::ensureNotCurvedType(geom->getGeometryN(ipoly));
+            util::ensureNoCurvedComponents(geom->getGeometryN(ipoly));
 
             const Polygon* poly = static_cast<const Polygon*>(geom->getGeometryN(ipoly));
 
diff --git a/src/coverage/CoverageValidator.cpp b/src/coverage/CoverageValidator.cpp
index fedad7b49..644558334 100644
--- a/src/coverage/CoverageValidator.cpp
+++ b/src/coverage/CoverageValidator.cpp
@@ -74,7 +74,7 @@ CoverageValidator::validate()
     TemplateSTRtree<const Geometry*> index;
     std::vector<std::unique_ptr<Geometry>> invalidLines;
     for (auto* geom : m_coverage) {
-        util::ensureNotCurvedType(geom);
+        util::ensureNoCurvedComponents(geom);
 
         index.insert(geom);
         invalidLines.emplace_back(nullptr);
diff --git a/src/geom/CircularString.cpp b/src/geom/CircularString.cpp
index 1bdaaf16d..f5bb375c8 100644
--- a/src/geom/CircularString.cpp
+++ b/src/geom/CircularString.cpp
@@ -21,16 +21,22 @@ namespace geos {
 namespace geom {
 
 /*public*/
-CircularString::CircularString(std::unique_ptr<CoordinateSequence> && newCoords,
+CircularString::CircularString(std::unique_ptr<CoordinateSequence>&& newCoords,
                                const GeometryFactory& factory)
     :
     SimpleCurve(std::move(newCoords), false, factory)
 {
-    //validateConstruction();
+    validateConstruction();
 }
 
 CircularString::~CircularString() = default;
 
+std::unique_ptr<CircularString>
+CircularString::clone() const
+{
+    return std::unique_ptr<CircularString>(cloneImpl());
+}
+
 std::string
 CircularString::getGeometryType() const
 {
@@ -43,12 +49,6 @@ CircularString::getGeometryTypeId() const
     return GEOS_CIRCULARSTRING;
 }
 
-std::unique_ptr<CircularString>
-CircularString::clone() const
-{
-    return std::unique_ptr<CircularString>(cloneImpl());
-}
-
 CircularString*
 CircularString::reverseImpl() const
 {
@@ -63,5 +63,18 @@ CircularString::reverseImpl() const
     return getFactory()->createCircularString(std::move(seq)).release();
 }
 
+void
+CircularString::validateConstruction()
+{
+    if (points.get() == nullptr) {
+        points = std::make_unique<CoordinateSequence>();
+        return;
+    }
+
+    if (points->size() == 2) {
+        throw util::IllegalArgumentException("point array must contain 0 or >2 elements\n");
+    }
+}
+
 }
 }
diff --git a/src/geom/CompoundCurve.cpp b/src/geom/CompoundCurve.cpp
index f44f54876..2175c7ed2 100644
--- a/src/geom/CompoundCurve.cpp
+++ b/src/geom/CompoundCurve.cpp
@@ -12,8 +12,8 @@
  *
  **********************************************************************/
 
-#include <geos/geom/CoordinateFilter.h>
 #include <geos/geom/CompoundCurve.h>
+#include <geos/geom/CoordinateFilter.h>
 #include <geos/geom/GeometryFactory.h>
 #include <geos/operation/BoundaryOp.h>
 #include <geos/util.h>
@@ -25,7 +25,7 @@ CompoundCurve::CompoundCurve(std::vector<std::unique_ptr<SimpleCurve>>&& p_curve
                              const GeometryFactory& gf)
     : Curve(gf),
       curves(std::move(p_curves)),
-    envelope(computeEnvelopeInternal()) {}
+      envelope(computeEnvelopeInternal()) {}
 
 CompoundCurve::CompoundCurve(const CompoundCurve& other)
     : Curve(other),
@@ -50,112 +50,69 @@ CompoundCurve::operator=(const CompoundCurve& other)
     return *this;
 }
 
-std::unique_ptr<CoordinateSequence>
-CompoundCurve::getCoordinates() const
+void
+CompoundCurve::apply_ro(CoordinateFilter* cf) const
 {
-    // FIXME: CoordinateSequence would have curved and linear sections?
-    auto ret = std::make_unique<CoordinateSequence>(0, hasZ(), hasM());
     for (const auto& curve : curves) {
-        ret->add(*curve->getCoordinatesRO());
+        curve->apply_ro(cf);
     }
-    return ret;
 }
 
-const CoordinateXY*
-CompoundCurve::getCoordinate() const
+void
+CompoundCurve::apply_ro(CoordinateSequenceFilter& csf) const
 {
     for (const auto& curve : curves) {
-        if (!curve->isEmpty()) {
-            return curve->getCoordinate();
+        const auto& seq = *curve->getCoordinatesRO();
+        for (std::size_t i = 0; i < seq.size(); i++) {
+            if (csf.isDone()) {
+                return;
+            }
+            csf.filter_ro(seq, i);
         }
     }
-
-    return nullptr;
 }
 
-uint8_t
-CompoundCurve::getCoordinateDimension() const
+void
+CompoundCurve::apply_rw(const CoordinateFilter* cf)
 {
-    return static_cast<std::uint8_t>(2 + hasZ() + hasM());
-}
-
-bool
-CompoundCurve::hasZ() const
-{
-    return std::any_of(curves.begin(), curves.end(), [](const auto& curve) {
-        return curve->hasZ();
-    });
-}
-
-bool
-CompoundCurve::hasM() const
-{
-    return std::any_of(curves.begin(), curves.end(), [](const auto& curve) {
-        return curve->hasM();
-    });
-}
-
-bool
-CompoundCurve::isEmpty() const
-{
-    return !std::any_of(curves.begin(), curves.end(), [](const auto& curve) {
-        return !curve->isEmpty();
-    });
-}
-
-bool
-CompoundCurve::isClosed() const
-{
-    if (isEmpty()) {
-        return false;
+    for (auto& curve : curves) {
+        curve->apply_rw(cf);
     }
-
-    const SimpleCurve& first = *curves.front();
-    const SimpleCurve& last = *curves.back();
-
-    return first.getCoordinateN(0) == last.getCoordinateN(last.getNumPoints() - 1);
 }
 
-std::size_t
-CompoundCurve::getNumPoints() const
+void
+CompoundCurve::apply_rw(CoordinateSequenceFilter&)
 {
-    std::size_t n =0;
+    throw util::UnsupportedOperationException();
+}
+
+std::unique_ptr<CompoundCurve>
+CompoundCurve::clone() const
+{
+    return std::unique_ptr<CompoundCurve>(cloneImpl());
+}
+
+CompoundCurve*
+CompoundCurve::cloneImpl() const
+{
+    return new CompoundCurve(*this);
+}
+
+int
+CompoundCurve::compareToSameClass(const Geometry* g) const
+{
+    const CompoundCurve* curve = detail::down_cast<const CompoundCurve*>(g);
+    return compare(curves, curve->curves);
+}
+
+Envelope
+CompoundCurve::computeEnvelopeInternal() const
+{
+    Envelope e;
     for (const auto& curve : curves) {
-        n += curve->getNumPoints();
+        e.expandToInclude(curve->getEnvelopeInternal());
     }
-    return n;
-}
-
-std::size_t
-CompoundCurve::getNumCurves() const
-{
-    return curves.size();
-}
-
-const SimpleCurve*
-CompoundCurve::getCurveN(std::size_t i) const
-{
-    return curves[i].get();
-}
-
-std::unique_ptr<Geometry>
-CompoundCurve::getBoundary() const
-{
-    operation::BoundaryOp bop(*this);
-    return bop.getBoundary();
-}
-
-
-std::string
-CompoundCurve::getGeometryType() const
-{
-    return "CompoundCurve";
-}
-
-GeometryTypeId
-CompoundCurve::getGeometryTypeId() const
-{
-    return GEOS_COMPOUNDCURVE;
+    return e;
 }
 
 bool
@@ -200,16 +157,137 @@ CompoundCurve::equalsIdentical(const Geometry* other) const
     return true;
 }
 
-std::unique_ptr<CompoundCurve>
-CompoundCurve::clone() const
+std::unique_ptr<Geometry>
+CompoundCurve::getBoundary() const
 {
-    return std::unique_ptr<CompoundCurve>(cloneImpl());
+    operation::BoundaryOp bop(*this);
+    return bop.getBoundary();
 }
 
-CompoundCurve*
-CompoundCurve::cloneImpl() const
+const CoordinateXY*
+CompoundCurve::getCoordinate() const
 {
-    return new CompoundCurve(*this);
+    for (const auto& curve : curves) {
+        if (!curve->isEmpty()) {
+            return curve->getCoordinate();
+        }
+    }
+
+    return nullptr;
+}
+
+uint8_t
+CompoundCurve::getCoordinateDimension() const
+{
+    return static_cast<std::uint8_t>(2 + hasZ() + hasM());
+}
+
+std::unique_ptr<CoordinateSequence>
+CompoundCurve::getCoordinates() const
+{
+    auto ret = std::make_unique<CoordinateSequence>(0, hasZ(), hasM());
+    for (const auto& curve : curves) {
+        ret->add(*curve->getCoordinatesRO());
+    }
+    return ret;
+}
+
+const SimpleCurve*
+CompoundCurve::getCurveN(std::size_t i) const
+{
+    return curves[i].get();
+}
+
+std::string
+CompoundCurve::getGeometryType() const
+{
+    return "CompoundCurve";
+}
+
+GeometryTypeId
+CompoundCurve::getGeometryTypeId() const
+{
+    return GEOS_COMPOUNDCURVE;
+}
+
+double
+CompoundCurve::getLength() const
+{
+    double sum = 0;
+    for (const auto& curve : curves) {
+        sum += curve->getLength();
+    }
+    return sum;
+}
+
+std::size_t
+CompoundCurve::getNumCurves() const
+{
+    return curves.size();
+}
+
+std::size_t
+CompoundCurve::getNumPoints() const
+{
+    std::size_t n =0;
+    for (const auto& curve : curves) {
+        n += curve->getNumPoints();
+    }
+    return n;
+}
+
+bool
+CompoundCurve::hasZ() const
+{
+    return std::any_of(curves.begin(), curves.end(), [](const auto& curve) {
+        return curve->hasZ();
+    });
+}
+
+bool
+CompoundCurve::hasM() const
+{
+    return std::any_of(curves.begin(), curves.end(), [](const auto& curve) {
+        return curve->hasM();
+    });
+}
+
+bool
+CompoundCurve::hasCurvedComponents() const
+{
+    for (const auto& curve : curves) {
+        if (curve->hasCurvedComponents()) {
+            return true;
+        }
+    }
+    return false;
+}
+
+bool
+CompoundCurve::isClosed() const
+{
+    if (isEmpty()) {
+        return false;
+    }
+
+    const SimpleCurve& first = *curves.front();
+    const SimpleCurve& last = *curves.back();
+
+    return first.getCoordinateN(0) == last.getCoordinateN(last.getNumPoints() - 1);
+}
+
+bool
+CompoundCurve::isEmpty() const
+{
+    return !std::any_of(curves.begin(), curves.end(), [](const auto& curve) {
+        return !curve->isEmpty();
+    });
+}
+
+void
+CompoundCurve::normalize()
+{
+    throw util::UnsupportedOperationException();
 }
 
 std::unique_ptr<CompoundCurve>
@@ -229,81 +307,5 @@ CompoundCurve::reverseImpl() const
     return getFactory()->createCompoundCurve(std::move(reversed)).release();
 }
 
-double CompoundCurve::getLength() const {
-    double sum = 0;
-    for (const auto& curve : curves) {
-        sum += curve->getLength();
-    }
-    return sum;
-}
-
-Envelope
-CompoundCurve::computeEnvelopeInternal() const {
-    Envelope e;
-    for (const auto& curve : curves) {
-        e.expandToInclude(curve->getEnvelopeInternal());
-    }
-    return e;
-}
-
-int
-CompoundCurve::compareToSameClass(const Geometry* g) const
-{
-    const CompoundCurve* curve = detail::down_cast<const CompoundCurve*>(g);
-    return compare(curves, curve->curves);
-}
-
-bool CompoundCurve::hasCurvedComponents() const {
-    for (const auto& curve : curves) {
-        if (curve->hasCurvedComponents()) {
-            return true;
-        }
-    }
-    return false;
-}
-
-void
-CompoundCurve::normalize()
-{
-    throw util::UnsupportedOperationException();
-}
-
-void
-CompoundCurve::apply_ro(CoordinateFilter* cf) const
-{
-    for (const auto& curve : curves) {
-        curve->apply_ro(cf);
-    }
-}
-
-void
-CompoundCurve::apply_ro(CoordinateSequenceFilter& csf) const
-{
-    for (const auto& curve : curves) {
-        const auto& seq = *curve->getCoordinatesRO();
-        for (std::size_t i = 0; i < seq.size(); i++) {
-            if (csf.isDone()) {
-                return;
-            }
-            csf.filter_ro(seq, i);
-        }
-    }
-}
-
-void
-CompoundCurve::apply_rw(const CoordinateFilter* cf)
-{
-    for (auto& curve : curves) {
-        curve->apply_rw(cf);
-    }
-}
-
-void
-CompoundCurve::apply_rw(CoordinateSequenceFilter&)
-{
-    throw util::UnsupportedOperationException();
-}
-
-
 }
 }
diff --git a/src/geom/Curve.cpp b/src/geom/Curve.cpp
index 4bd3d0244..91fd60da2 100644
--- a/src/geom/Curve.cpp
+++ b/src/geom/Curve.cpp
@@ -21,10 +21,10 @@ namespace geos {
 namespace geom {
 
 void
-Curve::apply_rw(GeometryFilter* filter)
+Curve::apply_ro(GeometryComponentFilter* filter) const
 {
     assert(filter);
-    filter->filter_rw(this);
+    filter->filter_ro(this);
 }
 
 void
@@ -42,10 +42,10 @@ Curve::apply_rw(GeometryComponentFilter* filter)
 }
 
 void
-Curve::apply_ro(GeometryComponentFilter* filter) const
+Curve::apply_rw(GeometryFilter* filter)
 {
     assert(filter);
-    filter->filter_ro(this);
+    filter->filter_rw(this);
 }
 
 bool
@@ -56,4 +56,4 @@ Curve::isRing() const
 
 
 }
-}
\ No newline at end of file
+}
diff --git a/src/geom/Geometry.cpp b/src/geom/Geometry.cpp
index 6c5b43c3b..8a18f2f93 100644
--- a/src/geom/Geometry.cpp
+++ b/src/geom/Geometry.cpp
@@ -205,7 +205,7 @@ Geometry::getCentroid(CoordinateXY& ret) const
 std::unique_ptr<Point>
 Geometry::getInteriorPoint() const
 {
-    geos::util::ensureNotCurvedType(this);
+    geos::util::ensureNoCurvedComponents(this);
 
     Coordinate interiorPt;
     int dim = getDimension();
@@ -798,25 +798,6 @@ Geometry::getPrecisionModel() const
     return _factory->getPrecisionModel();
 }
 
-bool
-Geometry::isCurvedType(GeometryTypeId typ) {
-    switch(typ) {
-        case GEOS_CIRCULARSTRING:
-        case GEOS_COMPOUNDCURVE:
-        case GEOS_CURVEPOLYGON:
-        case GEOS_MULTICURVE:
-        case GEOS_MULTISURFACE:
-            return true;
-        default:
-            return false;
-    }
-}
-
-bool
-Geometry::isCurvedType() const {
-    return isCurvedType(getGeometryTypeId());
-}
-
 bool
 Geometry::hasCurvedComponents() const {
     return false;
diff --git a/src/geom/MultiCurve.cpp b/src/geom/MultiCurve.cpp
index 9b4375094..31e13eb2c 100644
--- a/src/geom/MultiCurve.cpp
+++ b/src/geom/MultiCurve.cpp
@@ -14,29 +14,35 @@
  *
  **********************************************************************/
 
-#include <geos/geom/MultiCurve.h>
-#include <geos/util.h>
-#include <geos/operation/BoundaryOp.h>
 #include <geos/geom/GeometryFactory.h>
+#include <geos/geom/MultiCurve.h>
+#include <geos/operation/BoundaryOp.h>
+#include <geos/util.h>
 
 namespace geos {
 namespace geom {
 
-/*protected*/
+MultiCurve::MultiCurve(std::vector<std::unique_ptr<Geometry>>&& newLines,
+                       const GeometryFactory& factory)
+    : GeometryCollection(std::move(newLines), factory)
+{
+    for (const auto& geom : geometries) {
+        if (!dynamic_cast<const Curve*>(geom.get())) {
+            throw util::IllegalArgumentException("All elements of MultiCurve must be a Curve");
+        }
+    }
+}
+
 MultiCurve::MultiCurve(std::vector<std::unique_ptr<Curve>>&& newLines,
                        const GeometryFactory& factory)
     : GeometryCollection(std::move(newLines), factory)
 {}
 
-MultiCurve::MultiCurve(std::vector<std::unique_ptr<Geometry>>&& newLines,
-                       const GeometryFactory& factory)
-    : GeometryCollection(std::move(newLines), factory)
-{}
-
-Dimension::DimensionType
-MultiCurve::getDimension() const
+std::unique_ptr<Geometry>
+MultiCurve::getBoundary() const
 {
-    return Dimension::L; // line
+    operation::BoundaryOp bop(*this);
+    return bop.getBoundary();
 }
 
 int
@@ -48,12 +54,30 @@ MultiCurve::getBoundaryDimension() const
     return 0;
 }
 
+Dimension::DimensionType
+MultiCurve::getDimension() const
+{
+    return Dimension::L; // line
+}
+
+const Curve*
+MultiCurve::getGeometryN(std::size_t i) const
+{
+    return static_cast<const Curve*>(geometries[i].get());
+}
+
 std::string
 MultiCurve::getGeometryType() const
 {
     return "MultiCurve";
 }
 
+GeometryTypeId
+MultiCurve::getGeometryTypeId() const
+{
+    return GEOS_MULTICURVE;
+}
+
 bool
 MultiCurve::isClosed() const
 {
@@ -69,19 +93,6 @@ MultiCurve::isClosed() const
     return true;
 }
 
-std::unique_ptr<Geometry>
-MultiCurve::getBoundary() const
-{
-    operation::BoundaryOp bop(*this);
-    return bop.getBoundary();
-}
-
-GeometryTypeId
-MultiCurve::getGeometryTypeId() const
-{
-    return GEOS_MULTICURVE;
-}
-
 MultiCurve*
 MultiCurve::reverseImpl() const
 {
@@ -101,12 +112,5 @@ MultiCurve::reverseImpl() const
     return getFactory()->createMultiCurve(std::move(reversed)).release();
 }
 
-const Curve*
-MultiCurve::getGeometryN(std::size_t i) const
-{
-    return static_cast<const Curve*>(geometries[i].get());
-}
-
-
 }
 }
diff --git a/src/geom/MultiSurface.cpp b/src/geom/MultiSurface.cpp
index 82cc4c572..b8e299f6a 100644
--- a/src/geom/MultiSurface.cpp
+++ b/src/geom/MultiSurface.cpp
@@ -14,18 +14,21 @@
  *
  **********************************************************************/
 
-#include <geos/geom/MultiSurface.h>
-#include <geos/geom/MultiCurve.h>
 #include <geos/geom/GeometryFactory.h>
+#include <geos/geom/MultiCurve.h>
+#include <geos/geom/MultiSurface.h>
 
 namespace geos {
 namespace geom {
 
-/*protected*/
 MultiSurface::MultiSurface(std::vector<std::unique_ptr<Geometry>>&& newPolys, const GeometryFactory& factory)
     : GeometryCollection(std::move(newPolys), factory)
 {
-    // FIXME check that all elements are in fact surfaces
+    for (const auto& geom : geometries) {
+        if (!dynamic_cast<const Surface*>(geom.get())) {
+            throw util::IllegalArgumentException("All elements of MultiSurface must be a Surface");
+        }
+    }
 }
 
 MultiSurface::MultiSurface(std::vector<std::unique_ptr<Surface>>&& newPolys, const GeometryFactory& factory)
@@ -35,33 +38,12 @@ MultiSurface::MultiSurface(std::vector<std::unique_ptr<Surface>>&& newPolys, con
 
 MultiSurface::~MultiSurface() {}
 
-Dimension::DimensionType
-MultiSurface::getDimension() const
-{
-    return Dimension::A; // area
-}
-
-int
-MultiSurface::getBoundaryDimension() const
-{
-    return 1;
-}
-
-std::string
-MultiSurface::getGeometryType() const
-{
-    return "MultiSurface";
-}
-
 std::unique_ptr<Geometry>
 MultiSurface::getBoundary() const
 {
-    // FIXME implement
-#if 0
     if (isEmpty()) {
         return std::unique_ptr<Geometry>(getFactory()->createMultiCurve());
     }
-#endif
 
     std::vector<std::unique_ptr<Geometry>> allRings;
     for (const auto& pg : geometries) {
@@ -71,9 +53,8 @@ MultiSurface::getBoundary() const
             allRings.push_back(std::move(g));
         }
         else {
-            for (std::size_t i = 0; i < g->getNumGeometries(); ++i) {
-                // TODO avoid this clone
-                allRings.push_back(g->getGeometryN(i)->clone());
+            for (auto& gi : (static_cast<GeometryCollection&>(*g)).releaseGeometries()) {
+                allRings.push_back(std::move(gi));
             }
         }
     }
@@ -81,6 +62,24 @@ MultiSurface::getBoundary() const
     return getFactory()->createMultiCurve(std::move(allRings));
 }
 
+int
+MultiSurface::getBoundaryDimension() const
+{
+    return 1;
+}
+
+Dimension::DimensionType
+MultiSurface::getDimension() const
+{
+    return Dimension::A; // area
+}
+
+std::string
+MultiSurface::getGeometryType() const
+{
+    return "MultiSurface";
+}
+
 GeometryTypeId
 MultiSurface::getGeometryTypeId() const
 {
@@ -106,6 +105,5 @@ MultiSurface::reverseImpl() const
     return getFactory()->createMultiSurface(std::move(reversed)).release();
 }
 
-
 }
 }
diff --git a/src/geom/SimpleCurve.cpp b/src/geom/SimpleCurve.cpp
index a0e039760..def5aa1ea 100644
--- a/src/geom/SimpleCurve.cpp
+++ b/src/geom/SimpleCurve.cpp
@@ -40,23 +40,94 @@ SimpleCurve::SimpleCurve(const SimpleCurve& other)
 
 SimpleCurve::SimpleCurve(std::unique_ptr<CoordinateSequence>&& newCoords,
                          bool isLinear,
-            const GeometryFactory& factory)
+                         const GeometryFactory& factory)
     : Curve(factory),
-    points(newCoords ? std::move(newCoords) : std::make_unique<CoordinateSequence>()),
-    envelope(computeEnvelopeInternal(isLinear))
+      points(newCoords ? std::move(newCoords) : std::make_unique<CoordinateSequence>()),
+      envelope(computeEnvelopeInternal(isLinear))
 {
 }
 
+void
+SimpleCurve::apply_ro(CoordinateFilter* filter) const
+{
+    assert(points.get());
+    points->apply_ro(filter);
+}
+
+void
+SimpleCurve::apply_ro(CoordinateSequenceFilter& filter) const
+{
+    std::size_t npts = points->size();
+    if (!npts) {
+        return;
+    }
+    for (std::size_t i = 0; i < npts; ++i) {
+        filter.filter_ro(*points, i);
+        if (filter.isDone()) {
+            break;
+        }
+    }
+}
+
+void
+SimpleCurve::apply_rw(const CoordinateFilter* filter)
+{
+    assert(points.get());
+    points->apply_rw(filter);
+}
+
+void
+SimpleCurve::apply_rw(CoordinateSequenceFilter& filter)
+{
+    std::size_t npts = points->size();
+    if (!npts) {
+        return;
+    }
+    for (std::size_t i = 0; i < npts; ++i) {
+        filter.filter_rw(*points, i);
+        if (filter.isDone()) {
+            break;
+        }
+    }
+    if (filter.isGeometryChanged()) {
+        geometryChanged();
+    }
+}
+
+int
+SimpleCurve::compareToSameClass(const Geometry* ls) const
+{
+    const SimpleCurve* line = detail::down_cast<const SimpleCurve*>(ls);
+
+    // MD - optimized implementation
+    std::size_t mynpts = points->getSize();
+    std::size_t othnpts = line->points->getSize();
+    if (mynpts > othnpts) {
+        return 1;
+    }
+    if (mynpts < othnpts) {
+        return -1;
+    }
+    for (std::size_t i = 0; i < mynpts; i++) {
+        int cmp = points->getAt<CoordinateXY>(i).compareTo(line->points->getAt<CoordinateXY>(i));
+        if (cmp) {
+            return cmp;
+        }
+    }
+    return 0;
+}
+
 Envelope
 SimpleCurve::computeEnvelopeInternal(bool isLinear) const
 {
-    if(isEmpty()) {
+    if (isEmpty()) {
         return Envelope();
     }
 
-    if(isLinear) {
+    if (isLinear) {
         return points->getEnvelope();
-    } else {
+    }
+    else {
         Envelope e;
         for (std::size_t i = 2; i < points->size(); i++) {
             algorithm::CircularArcs::expandEnvelope(e,
@@ -68,6 +139,71 @@ SimpleCurve::computeEnvelopeInternal(bool isLinear) const
     }
 }
 
+bool
+SimpleCurve::equalsExact(const Geometry* other, double tolerance) const
+{
+    if (!isEquivalentClass(other)) {
+        return false;
+    }
+
+    const SimpleCurve* otherCurve = detail::down_cast<const SimpleCurve*>(other);
+    std::size_t npts = points->getSize();
+    if (npts != otherCurve->points->getSize()) {
+        return false;
+    }
+    for (std::size_t i = 0; i < npts; ++i) {
+        if (!equal(points->getAt<CoordinateXY>(i), otherCurve->points->getAt<CoordinateXY>(i), tolerance)) {
+            return false;
+        }
+    }
+    return true;
+}
+
+bool
+SimpleCurve::equalsIdentical(const Geometry* other_g) const
+{
+    if (!isEquivalentClass(other_g)) {
+        return false;
+    }
+
+    const auto& other = static_cast<const SimpleCurve&>(*other_g);
+
+    if (envelope != other.envelope) {
+        return false;
+    }
+
+    return getCoordinatesRO()->equalsIdentical(*other.getCoordinatesRO());
+}
+
+std::unique_ptr<Geometry>
+SimpleCurve::getBoundary() const
+{
+    operation::BoundaryOp bop(*this);
+    return bop.getBoundary();
+}
+
+const CoordinateXY*
+SimpleCurve::getCoordinate() const
+{
+    if (isEmpty()) {
+        return nullptr;
+    }
+    return &(points->getAt<CoordinateXY>(0));
+}
+
+uint8_t
+SimpleCurve::getCoordinateDimension() const
+{
+    return (uint8_t) points->getDimension();
+}
+
+const Coordinate&
+SimpleCurve::getCoordinateN(std::size_t n) const
+{
+    assert(points.get());
+    return points->getAt(n);
+}
+
 std::unique_ptr<CoordinateSequence>
 SimpleCurve::getCoordinates() const
 {
@@ -82,57 +218,13 @@ SimpleCurve::getCoordinatesRO() const
     return points.get();
 }
 
-std::unique_ptr<CoordinateSequence>
-SimpleCurve::releaseCoordinates()
+std::unique_ptr<Point>
+SimpleCurve::getEndPoint() const
 {
-    auto newPts = std::make_unique<CoordinateSequence>(0u, points->hasZ(), points->hasM());
-    auto ret = std::move(points);
-    points = std::move(newPts);
-    geometryChanged();
-    return ret;
-}
-
-
-const Coordinate&
-SimpleCurve::getCoordinateN(std::size_t n) const
-{
-    assert(points.get());
-    return points->getAt(n);
-}
-
-uint8_t
-SimpleCurve::getCoordinateDimension() const
-{
-    return (uint8_t) points->getDimension();
-}
-
-bool
-SimpleCurve::hasM() const
-{
-    return points->hasM();
-}
-
-bool
-SimpleCurve::hasZ() const
-{
-    return points->hasZ();
-}
-
-int
-SimpleCurve::getBoundaryDimension() const
-{
-    if(isClosed()) {
-        return Dimension::False;
+    if (isEmpty()) {
+        return nullptr;
     }
-    return 0;
-}
-
-
-bool
-SimpleCurve::isEmpty() const
-{
-    assert(points.get());
-    return points->isEmpty();
+    return getPointN(getNumPoints() - 1);
 }
 
 std::size_t
@@ -153,125 +245,84 @@ SimpleCurve::getPointN(std::size_t n) const
 std::unique_ptr<Point>
 SimpleCurve::getStartPoint() const
 {
-    if(isEmpty()) {
+    if (isEmpty()) {
         return nullptr;
     }
     return getPointN(0);
 }
 
-std::unique_ptr<Point>
-SimpleCurve::getEndPoint() const
+bool
+SimpleCurve::hasM() const
 {
-    if(isEmpty()) {
-        return nullptr;
-    }
-    return getPointN(getNumPoints() - 1);
+    return points->hasM();
+}
+
+bool
+SimpleCurve::hasZ() const
+{
+    return points->hasZ();
 }
 
 bool
 SimpleCurve::isClosed() const
 {
-    if(isEmpty()) {
+    if (isEmpty()) {
         return false;
     }
 
     return points->front<CoordinateXY>().equals2D(points->back<CoordinateXY>());
 }
 
-std::unique_ptr<Geometry>
-SimpleCurve::getBoundary() const
-{
-    operation::BoundaryOp bop(*this);
-    return bop.getBoundary();
-}
-
 bool
 SimpleCurve::isCoordinate(CoordinateXY& pt) const
 {
     assert(points.get());
     std::size_t npts = points->getSize();
-    for(std::size_t i = 0; i < npts; i++) {
-        if(points->getAt<CoordinateXY>(i) == pt) {
+    for (std::size_t i = 0; i < npts; i++) {
+        if (points->getAt<CoordinateXY>(i) == pt) {
             return true;
         }
     }
     return false;
 }
 
-const CoordinateXY*
-SimpleCurve::getCoordinate() const
+bool
+SimpleCurve::isEmpty() const
 {
-    if(isEmpty()) {
-        return nullptr;
-    }
-    return &(points->getAt<CoordinateXY>(0));
+    assert(points.get());
+    return points->isEmpty();
 }
 
-bool
-SimpleCurve::equalsExact(const Geometry* other, double tolerance) const
+/*public*/
+void
+SimpleCurve::normalize()
 {
-    if(!isEquivalentClass(other)) {
-        return false;
-    }
+    util::ensureNoCurvedComponents(*this);
 
-    const SimpleCurve* otherCurve = detail::down_cast<const SimpleCurve*>(other);
+    if (isEmpty()) return;
+    assert(points.get());
+    if (isClosed()) {
+        normalizeClosed();
+        return;
+    }
     std::size_t npts = points->getSize();
-    if(npts != otherCurve->points->getSize()) {
-        return false;
-    }
-    for(std::size_t i = 0; i < npts; ++i) {
-        if(!equal(points->getAt<CoordinateXY>(i), otherCurve->points->getAt<CoordinateXY>(i), tolerance)) {
-            return false;
+    std::size_t n = npts / 2;
+    for (std::size_t i = 0; i < n; i++) {
+        std::size_t j = npts - 1 - i;
+        if (!(points->getAt<CoordinateXY>(i) == points->getAt<CoordinateXY>(j))) {
+            if (points->getAt<CoordinateXY>(i).compareTo(points->getAt<CoordinateXY>(j)) > 0) {
+                points->reverse();
+            }
+            return;
         }
     }
-    return true;
 }
 
-bool
-SimpleCurve::equalsIdentical(const Geometry* other_g) const
-{
-    if(!isEquivalentClass(other_g)) {
-        return false;
-    }
-
-    const auto& other = static_cast<const SimpleCurve&>(*other_g);
-
-    if (envelope != other.envelope) {
-        return false;
-    }
-
-    return getCoordinatesRO()->equalsIdentical(*other.getCoordinatesRO());
-}
-
-int
-SimpleCurve::compareToSameClass(const Geometry* ls) const
-{
-    const SimpleCurve* line = detail::down_cast<const SimpleCurve*>(ls);
-
-    // MD - optimized implementation
-    std::size_t mynpts = points->getSize();
-    std::size_t othnpts = line->points->getSize();
-    if(mynpts > othnpts) {
-        return 1;
-    }
-    if(mynpts < othnpts) {
-        return -1;
-    }
-    for(std::size_t i = 0; i < mynpts; i++) {
-        int cmp = points->getAt<CoordinateXY>(i).compareTo(line->points->getAt<CoordinateXY>(i));
-        if(cmp) {
-            return cmp;
-        }
-    }
-    return 0;
-}
-
-
 /*private*/
 void
 SimpleCurve::normalizeClosed()
 {
-    if(isEmpty()) {
+    if (isEmpty()) {
         return;
     }
 
@@ -287,87 +338,21 @@ SimpleCurve::normalizeClosed()
     CoordinateSequence::scroll(coords.get(), minCoordinate);
     coords->closeRing(true);
 
-    if(coords->size() >= 4 && algorithm::Orientation::isCCW(coords.get())) {
+    if (coords->size() >= 4 && algorithm::Orientation::isCCW(coords.get())) {
         coords->reverse();
     }
 
     points = std::move(coords);
 }
 
-
-/*public*/
-void
-SimpleCurve::normalize()
+std::unique_ptr<CoordinateSequence>
+SimpleCurve::releaseCoordinates()
 {
-    util::ensureNotCurvedType(*this);
-
-    if (isEmpty()) return;
-    assert(points.get());
-    if (isClosed()) {
-        normalizeClosed();
-        return;
-    }
-    std::size_t npts = points->getSize();
-    std::size_t n = npts / 2;
-    for(std::size_t i = 0; i < n; i++) {
-        std::size_t j = npts - 1 - i;
-        if(!(points->getAt<CoordinateXY>(i) == points->getAt<CoordinateXY>(j))) {
-            if(points->getAt<CoordinateXY>(i).compareTo(points->getAt<CoordinateXY>(j)) > 0) {
-                points->reverse();
-            }
-            return;
-        }
-    }
-}
-
-
-void
-SimpleCurve::apply_rw(const CoordinateFilter* filter)
-{
-    assert(points.get());
-    points->apply_rw(filter);
-}
-
-void
-SimpleCurve::apply_ro(CoordinateFilter* filter) const
-{
-    assert(points.get());
-    points->apply_ro(filter);
-}
-
-
-void
-SimpleCurve::apply_rw(CoordinateSequenceFilter& filter)
-{
-    std::size_t npts = points->size();
-    if(!npts) {
-        return;
-    }
-    for(std::size_t i = 0; i < npts; ++i) {
-        filter.filter_rw(*points, i);
-        if(filter.isDone()) {
-            break;
-        }
-    }
-    if(filter.isGeometryChanged()) {
-        geometryChanged();
-    }
-}
-
-void
-SimpleCurve::apply_ro(CoordinateSequenceFilter& filter) const
-{
-    std::size_t npts = points->size();
-    if(!npts) {
-        return;
-    }
-    for(std::size_t i = 0; i < npts; ++i) {
-        filter.filter_ro(*points, i);
-        if(filter.isDone()) {
-            break;
-        }
-    }
-    //if (filter.isGeometryChanged()) geometryChanged();
+    auto newPts = std::make_unique<CoordinateSequence>(0u, points->hasZ(), points->hasM());
+    auto ret = std::move(points);
+    points = std::move(newPts);
+    geometryChanged();
+    return ret;
 }
 
 } // namespace geos::geom
diff --git a/src/geom/Surface.cpp b/src/geom/Surface.cpp
index 343dfb1e8..34bb5c601 100644
--- a/src/geom/Surface.cpp
+++ b/src/geom/Surface.cpp
@@ -37,26 +37,15 @@ Surface::apply_ro(CoordinateFilter* filter) const
 }
 
 void
-Surface::apply_rw(const CoordinateFilter* filter)
+Surface::apply_ro(CoordinateSequenceFilter& filter) const
 {
-    getExteriorRing()->apply_rw(filter);
-    for (std::size_t i = 0; i < getNumInteriorRing(); i++) {
-        getInteriorRingN(i)->apply_rw(filter);
+    getExteriorRing()->apply_ro(filter);
+
+    for (std::size_t i = 0; !filter.isDone() && i < getNumInteriorRing(); i++) {
+        getInteriorRingN(i)->apply_ro(filter);
     }
 }
 
-void
-Surface::apply_rw(GeometryFilter* filter)
-{
-    filter->filter_rw(this);
-}
-
-void
-Surface::apply_ro(GeometryFilter* filter) const
-{
-    filter->filter_ro(this);
-}
-
 void
 Surface::apply_ro(GeometryComponentFilter* filter) const
 {
@@ -68,11 +57,16 @@ Surface::apply_ro(GeometryComponentFilter* filter) const
 }
 
 void
-Surface::apply_rw(GeometryComponentFilter* filter)
+Surface::apply_ro(GeometryFilter* filter) const
+{
+    filter->filter_ro(this);
+}
+
+void
+Surface::apply_rw(const CoordinateFilter* filter)
 {
-    filter->filter_rw(this);
     getExteriorRing()->apply_rw(filter);
-    for (std::size_t i = 0; !filter->isDone() && i < getNumInteriorRing(); i++) {
+    for (std::size_t i = 0; i < getNumInteriorRing(); i++) {
         getInteriorRingN(i)->apply_rw(filter);
     }
 }
@@ -92,15 +86,21 @@ Surface::apply_rw(CoordinateSequenceFilter& filter)
 }
 
 void
-Surface::apply_ro(CoordinateSequenceFilter& filter) const
+Surface::apply_rw(GeometryComponentFilter* filter)
 {
-    getExteriorRing()->apply_ro(filter);
-
-    for (std::size_t i = 0; !filter.isDone() && i < getNumInteriorRing(); i++) {
-        getInteriorRingN(i)->apply_ro(filter);
+    filter->filter_rw(this);
+    getExteriorRing()->apply_rw(filter);
+    for (std::size_t i = 0; !filter->isDone() && i < getNumInteriorRing(); i++) {
+        getInteriorRingN(i)->apply_rw(filter);
     }
 }
 
+void
+Surface::apply_rw(GeometryFilter* filter)
+{
+    filter->filter_rw(this);
+}
+
 int
 Surface::compareToSameClass(const Geometry* g) const
 {
@@ -136,6 +136,11 @@ Surface::convexHull() const
     return getExteriorRing()->convexHull();
 }
 
+std::unique_ptr<Geometry>
+Surface::createEmptyRing(const GeometryFactory& factory)
+{
+    return factory.createLinearRing();
+}
 
 bool
 Surface::equalsExact(const Geometry* other, double tolerance) const
@@ -222,6 +227,17 @@ Surface::getEnvelopeInternal() const
     return getExteriorRing()->getEnvelopeInternal();
 }
 
+double
+Surface::getLength() const
+{
+    double len = 0.0;
+    len += getExteriorRing()->getLength();
+    for (std::size_t i = 0; i < getNumInteriorRing(); i++) {
+        len += getInteriorRingN(i)->getLength();
+    }
+    return len;
+}
+
 size_t
 Surface::getNumPoints() const
 {
@@ -266,20 +282,5 @@ Surface::isEmpty() const
     return getExteriorRing()->isEmpty();
 }
 
-double Surface::getLength() const {
-    double len = 0.0;
-    len += getExteriorRing()->getLength();
-    for(std::size_t i = 0; i < getNumInteriorRing(); i++) {
-        len += getInteriorRingN(i)->getLength();
-    }
-    return len;
-}
-
-std::unique_ptr<Geometry>
-Surface::createEmptyRing(const GeometryFactory& factory)
-{
-    return factory.createLinearRing();
-}
-
 }
 }
diff --git a/src/geom/prep/PreparedGeometryFactory.cpp b/src/geom/prep/PreparedGeometryFactory.cpp
index 565e9e21e..844596935 100644
--- a/src/geom/prep/PreparedGeometryFactory.cpp
+++ b/src/geom/prep/PreparedGeometryFactory.cpp
@@ -45,7 +45,7 @@ PreparedGeometryFactory::create(const geom::Geometry* g) const
         throw util::IllegalArgumentException("PreparedGeometry constructed with null Geometry object");
     }
 
-    util::ensureNotCurvedType(g);
+    util::ensureNoCurvedComponents(g);
 
     std::unique_ptr<PreparedGeometry> pg;
 
diff --git a/src/geom/prep/PreparedLineString.cpp b/src/geom/prep/PreparedLineString.cpp
index 728490571..4ae7f0ae8 100644
--- a/src/geom/prep/PreparedLineString.cpp
+++ b/src/geom/prep/PreparedLineString.cpp
@@ -57,7 +57,7 @@ PreparedLineString::getIntersectionFinder()
 bool
 PreparedLineString::intersects(const geom::Geometry* g) const
 {
-    geos::util::ensureNotCurvedType(g);
+    geos::util::ensureNoCurvedComponents(g);
 
     if(! envelopesIntersect(g)) {
         return false;
diff --git a/src/geom/prep/PreparedPoint.cpp b/src/geom/prep/PreparedPoint.cpp
index 88aac78b3..5ddef6602 100644
--- a/src/geom/prep/PreparedPoint.cpp
+++ b/src/geom/prep/PreparedPoint.cpp
@@ -29,7 +29,7 @@ namespace prep { // geos.geom.prep
 bool
 PreparedPoint::intersects(const geom::Geometry* g) const
 {
-    util::ensureNotCurvedType(g);
+    util::ensureNoCurvedComponents(g);
 
     if(! envelopesIntersect(g)) {
         return false;
diff --git a/src/geom/prep/PreparedPolygon.cpp b/src/geom/prep/PreparedPolygon.cpp
index c77b828a3..cf2243217 100644
--- a/src/geom/prep/PreparedPolygon.cpp
+++ b/src/geom/prep/PreparedPolygon.cpp
@@ -136,7 +136,7 @@ bool
 PreparedPolygon::
 intersects(const geom::Geometry* g) const
 {
-    geos::util::ensureNotCurvedType(g);
+    geos::util::ensureNoCurvedComponents(g);
 
     // envelope test
     if(!envelopesIntersect(g)) {
diff --git a/src/geom/util/GeometryEditor.cpp b/src/geom/util/GeometryEditor.cpp
index bbeaf98fb..acba50712 100644
--- a/src/geom/util/GeometryEditor.cpp
+++ b/src/geom/util/GeometryEditor.cpp
@@ -74,7 +74,7 @@ GeometryEditor::GeometryEditor(const GeometryFactory* newFactory)
 std::unique_ptr<Geometry>
 GeometryEditor::edit(const Geometry* geometry, GeometryEditorOperation* operation)
 {
-    geos::util::ensureNotCurvedType(geometry);
+    geos::util::ensureNoCurvedComponents(geometry);
 
     // if client did not supply a GeometryFactory, use the one from the input Geometry
     if(factory == nullptr) {
diff --git a/src/geomgraph/GeometryGraph.cpp b/src/geomgraph/GeometryGraph.cpp
index 5f47861ae..48e5f6de2 100644
--- a/src/geomgraph/GeometryGraph.cpp
+++ b/src/geomgraph/GeometryGraph.cpp
@@ -169,7 +169,7 @@ void
 GeometryGraph::add(const Geometry* g)
 //throw (UnsupportedOperationException *)
 {
-    util::ensureNotCurvedType(g);
+    util::ensureNoCurvedComponents(g);
 
     if(g->isEmpty()) {
         return;
diff --git a/src/io/GeoJSONWriter.cpp b/src/io/GeoJSONWriter.cpp
index f17455105..0fd4bc297 100644
--- a/src/io/GeoJSONWriter.cpp
+++ b/src/io/GeoJSONWriter.cpp
@@ -176,7 +176,7 @@ void GeoJSONWriter::encodeFeatureCollection(const geom::Geometry* g, geos_nlohma
 
 void GeoJSONWriter::encodeGeometry(const geom::Geometry* geometry, geos_nlohmann::ordered_json& j)
 {
-    util::ensureNotCurvedType(geometry);
+    util::ensureNoCurvedComponents(geometry);
 
     auto type = geometry->getGeometryTypeId();
     if (type == GEOS_POINT) {
diff --git a/src/io/WKBWriter.cpp b/src/io/WKBWriter.cpp
index dd82b58b6..0e622b0d4 100644
--- a/src/io/WKBWriter.cpp
+++ b/src/io/WKBWriter.cpp
@@ -99,7 +99,7 @@ WKBWriter::writeHEX(const Geometry& g, std::ostream& os)
 void
 WKBWriter::write(const Geometry& g, std::ostream& os)
 {
-    util::ensureNotCurvedType(g);
+    util::ensureNoCurvedComponents(g);
 
     OrdinateSet inputOrdinates = OrdinateSet::createXY();
     inputOrdinates.setM(g.hasM());
diff --git a/src/noding/GeometryNoder.cpp b/src/noding/GeometryNoder.cpp
index 65a7c9941..71ad91ea9 100644
--- a/src/noding/GeometryNoder.cpp
+++ b/src/noding/GeometryNoder.cpp
@@ -92,7 +92,7 @@ GeometryNoder::GeometryNoder(const geom::Geometry& g)
     :
     argGeom(g)
 {
-    util::ensureNotCurvedType(argGeom);
+    util::ensureNoCurvedComponents(argGeom);
 }
 
 /* private */
diff --git a/src/operation/BoundaryOp.cpp b/src/operation/BoundaryOp.cpp
index acc4a316a..b407a2a41 100644
--- a/src/operation/BoundaryOp.cpp
+++ b/src/operation/BoundaryOp.cpp
@@ -52,7 +52,7 @@ BoundaryOp::BoundaryOp(const geom::Geometry& geom, const algorithm::BoundaryNode
 std::unique_ptr<geom::Geometry>
 BoundaryOp::getBoundary()
 {
-    util::ensureNotCurvedType(m_geom);
+    util::ensureNoCurvedComponents(m_geom);
 
     if (auto ls = dynamic_cast<const LineString*>(&m_geom)) {
         return boundaryLineString(*ls);
diff --git a/src/operation/distance/DistanceOp.cpp b/src/operation/distance/DistanceOp.cpp
index 5ca7f941e..f0d2efa9b 100644
--- a/src/operation/distance/DistanceOp.cpp
+++ b/src/operation/distance/DistanceOp.cpp
@@ -104,8 +104,8 @@ DistanceOp::distance()
 {
     using geos::util::IllegalArgumentException;
 
-    util::ensureNotCurvedType(geom[0]);
-    util::ensureNotCurvedType(geom[1]);
+    util::ensureNoCurvedComponents(geom[0]);
+    util::ensureNoCurvedComponents(geom[1]);
 
     if(geom[0] == nullptr || geom[1] == nullptr) {
         throw IllegalArgumentException("null geometries are not supported");
diff --git a/src/operation/linemerge/LineMerger.cpp b/src/operation/linemerge/LineMerger.cpp
index 1f90ca4ac..e26aab299 100644
--- a/src/operation/linemerge/LineMerger.cpp
+++ b/src/operation/linemerge/LineMerger.cpp
@@ -94,7 +94,7 @@ struct LMGeometryComponentFilter: public GeometryComponentFilter {
 void
 LineMerger::add(const Geometry* geometry)
 {
-    util::ensureNotCurvedType(geometry);
+    util::ensureNoCurvedComponents(geometry);
 
     LMGeometryComponentFilter lmgcf(this);
     geometry->apply_ro(&lmgcf);
diff --git a/src/operation/overlayng/OverlayNGRobust.cpp b/src/operation/overlayng/OverlayNGRobust.cpp
index 2c7604014..96dda0f69 100644
--- a/src/operation/overlayng/OverlayNGRobust.cpp
+++ b/src/operation/overlayng/OverlayNGRobust.cpp
@@ -83,8 +83,8 @@ OverlayNGRobust::Union(const Geometry* a)
 std::unique_ptr<Geometry>
 OverlayNGRobust::Overlay(const Geometry* geom0, const Geometry* geom1, int opCode)
 {
-    geos::util::ensureNotCurvedType(geom0);
-    geos::util::ensureNotCurvedType(geom1);
+    geos::util::ensureNoCurvedComponents(geom0);
+    geos::util::ensureNoCurvedComponents(geom1);
 
     std::unique_ptr<Geometry> result;
     std::runtime_error exOriginal("");
diff --git a/src/operation/polygonize/Polygonizer.cpp b/src/operation/polygonize/Polygonizer.cpp
index cc8abccb0..92eefdfc0 100644
--- a/src/operation/polygonize/Polygonizer.cpp
+++ b/src/operation/polygonize/Polygonizer.cpp
@@ -114,7 +114,7 @@ Polygonizer::add(std::vector<const Geometry*>* geomList)
 void
 Polygonizer::add(const Geometry* g)
 {
-    util::ensureNotCurvedType(g);
+    util::ensureNoCurvedComponents(g);
     g->apply_ro(&lineStringAdder);
 }
 
diff --git a/src/triangulate/DelaunayTriangulationBuilder.cpp b/src/triangulate/DelaunayTriangulationBuilder.cpp
index 70bb217a9..cd80ecdcd 100644
--- a/src/triangulate/DelaunayTriangulationBuilder.cpp
+++ b/src/triangulate/DelaunayTriangulationBuilder.cpp
@@ -77,7 +77,7 @@ DelaunayTriangulationBuilder::DelaunayTriangulationBuilder() :
 void
 DelaunayTriangulationBuilder::setSites(const Geometry& geom)
 {
-    util::ensureNotCurvedType(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 29c05fee9..eeae7f064 100644
--- a/src/triangulate/VoronoiDiagramBuilder.cpp
+++ b/src/triangulate/VoronoiDiagramBuilder.cpp
@@ -51,7 +51,7 @@ VoronoiDiagramBuilder::VoronoiDiagramBuilder() :
 void
 VoronoiDiagramBuilder::setSites(const geom::Geometry& geom)
 {
-    util::ensureNotCurvedType(geom);
+    util::ensureNoCurvedComponents(geom);
     siteCoords = DelaunayTriangulationBuilder::extractUniqueCoordinates(geom);
     inputGeom = &geom;
 }
diff --git a/src/triangulate/polygon/ConstrainedDelaunayTriangulator.cpp b/src/triangulate/polygon/ConstrainedDelaunayTriangulator.cpp
index 4788d3ba5..5f56b12c2 100644
--- a/src/triangulate/polygon/ConstrainedDelaunayTriangulator.cpp
+++ b/src/triangulate/polygon/ConstrainedDelaunayTriangulator.cpp
@@ -41,7 +41,7 @@ ConstrainedDelaunayTriangulator::triangulate(const Geometry* geom)
 std::unique_ptr<Geometry>
 ConstrainedDelaunayTriangulator::compute() const
 {
-    util::ensureNotCurvedType(inputGeom);
+    util::ensureNoCurvedComponents(inputGeom);
 
     // short circuit empty input case
     if(inputGeom->isEmpty()) {

commit 60e0ae2b8f5d1df0d95cfe95eba2ba4f1cbd816e
Author: Daniel Baston <dbaston at gmail.com>
Date:   Mon Apr 15 19:39:11 2024 -0400

    DistanceOp: Avoid heap allocation of GeometryLocations

diff --git a/include/geos/operation/distance/ConnectedElementLocationFilter.h b/include/geos/operation/distance/ConnectedElementLocationFilter.h
index 0c6ab18e2..82e4261ea 100644
--- a/include/geos/operation/distance/ConnectedElementLocationFilter.h
+++ b/include/geos/operation/distance/ConnectedElementLocationFilter.h
@@ -50,7 +50,7 @@ namespace distance { // geos::operation::distance
 class GEOS_DLL ConnectedElementLocationFilter: public geom::GeometryFilter {
 private:
 
-    std::vector<std::unique_ptr<GeometryLocation>> locations;
+    std::vector<GeometryLocation> locations;
     ConnectedElementLocationFilter() = default;
     ConnectedElementLocationFilter(const ConnectedElementLocationFilter&) = delete;
     ConnectedElementLocationFilter& operator=(const ConnectedElementLocationFilter&) = delete;
@@ -63,7 +63,7 @@ public:
      * an empty list will be returned. The elements of the list
      * are [GeometryLocations](@ref operation::distance::GeometryLocation).
      */
-    static std::vector<std::unique_ptr<GeometryLocation>> getLocations(const geom::Geometry* geom);
+    static std::vector<GeometryLocation> getLocations(const geom::Geometry* geom);
 
     void filter_ro(const geom::Geometry* geom) override;
     void filter_rw(geom::Geometry* geom) override;
diff --git a/include/geos/operation/distance/DistanceOp.h b/include/geos/operation/distance/DistanceOp.h
index da13ab130..b4d6fed8b 100644
--- a/include/geos/operation/distance/DistanceOp.h
+++ b/include/geos/operation/distance/DistanceOp.h
@@ -172,19 +172,19 @@ private:
 
     // working
     algorithm::PointLocator ptLocator;
-    std::array<std::unique_ptr<GeometryLocation>, 2> minDistanceLocation;
+    std::array<GeometryLocation, 2> minDistanceLocation;
     double minDistance;
     bool computed = false;
 
-    void updateMinDistance(std::array<std::unique_ptr<GeometryLocation>, 2> & locGeom, bool flip);
+    void updateMinDistance(std::array<GeometryLocation, 2> & locGeom, bool flip);
 
     void computeMinDistance();
 
     void computeContainmentDistance();
 
-    void computeInside(std::vector<std::unique_ptr<GeometryLocation>> & locs,
+    void computeInside(std::vector<GeometryLocation> & locs,
                        const std::vector<const geom::Polygon*>& polys,
-                       std::array<std::unique_ptr<GeometryLocation>, 2> & locPtPoly);
+                       std::array<GeometryLocation, 2> & locPtPoly);
 
 
     /**
@@ -196,25 +196,25 @@ private:
     void computeMinDistanceLines(
         const std::vector<const geom::LineString*>& lines0,
         const std::vector<const geom::LineString*>& lines1,
-        std::array<std::unique_ptr<GeometryLocation>, 2> & locGeom);
+        std::array<GeometryLocation, 2> & locGeom);
 
     void computeMinDistancePoints(
         const std::vector<const geom::Point*>& points0,
         const std::vector<const geom::Point*>& points1,
-        std::array<std::unique_ptr<GeometryLocation>, 2> & locGeom);
+        std::array<GeometryLocation, 2> & locGeom);
 
     void computeMinDistanceLinesPoints(
         const std::vector<const geom::LineString*>& lines0,
         const std::vector<const geom::Point*>& points1,
-        std::array<std::unique_ptr<GeometryLocation>, 2> & locGeom);
+        std::array<GeometryLocation, 2> & locGeom);
 
     void computeMinDistance(const geom::LineString* line0,
                             const geom::LineString* line1,
-                            std::array<std::unique_ptr<GeometryLocation>, 2> & locGeom);
+                            std::array<GeometryLocation, 2> & locGeom);
 
     void computeMinDistance(const geom::LineString* line,
                             const geom::Point* pt,
-                            std::array<std::unique_ptr<GeometryLocation>, 2> & locGeom);
+                            std::array<GeometryLocation, 2> & locGeom);
 };
 
 
diff --git a/include/geos/operation/distance/GeometryLocation.h b/include/geos/operation/distance/GeometryLocation.h
index 764c6e7b0..1fb443762 100644
--- a/include/geos/operation/distance/GeometryLocation.h
+++ b/include/geos/operation/distance/GeometryLocation.h
@@ -62,6 +62,13 @@ public:
      */
     static const int INSIDE_AREA = -1;
 
+    GeometryLocation() :
+        component(nullptr),
+        segIndex(0),
+        inside_area(false),
+        pt()
+    {}
+
     /** \brief
      * Constructs a GeometryLocation specifying a point on a geometry,
      * as well as the segment that the point is on (or INSIDE_AREA if
diff --git a/src/operation/distance/ConnectedElementLocationFilter.cpp b/src/operation/distance/ConnectedElementLocationFilter.cpp
index 71feb2801..b117627b1 100644
--- a/src/operation/distance/ConnectedElementLocationFilter.cpp
+++ b/src/operation/distance/ConnectedElementLocationFilter.cpp
@@ -35,7 +35,7 @@ namespace operation { // geos.operation
 namespace distance { // geos.operation.distance
 
 /*public*/
-std::vector<std::unique_ptr<GeometryLocation>>
+std::vector<GeometryLocation>
 ConnectedElementLocationFilter::getLocations(const Geometry* geom)
 {
     ConnectedElementLocationFilter c;
@@ -51,7 +51,7 @@ ConnectedElementLocationFilter::filter_ro(const Geometry* geom)
             (typeid(*geom) == typeid(LineString)) ||
             (typeid(*geom) == typeid(LinearRing)) ||
             (typeid(*geom) == typeid(Polygon))) {
-        locations.emplace_back(new GeometryLocation(geom, 0, *(geom->getCoordinate())));
+        locations.emplace_back(geom, 0, *(geom->getCoordinate()));
     }
 }
 
@@ -64,7 +64,7 @@ ConnectedElementLocationFilter::filter_rw(Geometry* geom)
             (typeid(*geom) == typeid(LineString)) ||
             (typeid(*geom) == typeid(LinearRing)) ||
             (typeid(*geom) == typeid(Polygon))) {
-        locations.emplace_back(new GeometryLocation(geom, 0, *(geom->getCoordinate())));
+        locations.emplace_back(geom, 0, *(geom->getCoordinate()));
     }
 }
 
diff --git a/src/operation/distance/DistanceOp.cpp b/src/operation/distance/DistanceOp.cpp
index fc19c31f7..d587e755d 100644
--- a/src/operation/distance/DistanceOp.cpp
+++ b/src/operation/distance/DistanceOp.cpp
@@ -128,26 +128,26 @@ DistanceOp::nearestPoints()
     auto& locs = minDistanceLocation;
 
     // Empty input geometries result in this behaviour
-    if(locs[0] == nullptr || locs[1] == nullptr) {
+    if(locs[0].getGeometryComponent() == nullptr || locs[1].getGeometryComponent() == nullptr) {
         // either both or none are set..
-        assert(locs[0] == nullptr && locs[1] == nullptr);
+        assert(locs[0].getGeometryComponent() == nullptr && locs[1].getGeometryComponent() == nullptr);
 
         return nullptr;
     }
 
     auto nearestPts = detail::make_unique<CoordinateSequence>(2u);
-    nearestPts->setAt(locs[0]->getCoordinate(), 0);
-    nearestPts->setAt(locs[1]->getCoordinate(), 1);
+    nearestPts->setAt(locs[0].getCoordinate(), 0);
+    nearestPts->setAt(locs[1].getCoordinate(), 1);
 
     return nearestPts;
 }
 
 void
-DistanceOp::updateMinDistance(std::array<std::unique_ptr<GeometryLocation>, 2> & locGeom, bool flip)
+DistanceOp::updateMinDistance(std::array<GeometryLocation, 2> & locGeom, bool flip)
 {
     // if not set then don't update
-    if(locGeom[0] == nullptr) {
-        assert(locGeom[1] == nullptr);
+    if(locGeom[0].getGeometryComponent() == nullptr) {
+        assert(locGeom[1].getGeometryComponent() == nullptr);
 #if GEOS_DEBUG
         std::cerr << "updateMinDistance called with loc[0] == null and loc[1] == null" << std::endl;
 #endif
@@ -210,15 +210,15 @@ DistanceOp::computeContainmentDistance()
     // Expected to fill minDistanceLocation items
     // if minDistance <= terminateDistance
 
-    std::array<std::unique_ptr<GeometryLocation>, 2> locPtPoly;
+    std::array<GeometryLocation, 2> locPtPoly;
     // test if either geometry has a vertex inside the other
     if(! polys1.empty()) {
         auto insideLocs0 = ConnectedElementLocationFilter::getLocations(geom[0]);
         computeInside(insideLocs0, polys1, locPtPoly);
 
         if(minDistance <= terminateDistance) {
-            assert(locPtPoly[0]);
-            assert(locPtPoly[1]);
+            assert(locPtPoly[0].getGeometryComponent());
+            assert(locPtPoly[1].getGeometryComponent());
 
             minDistanceLocation[0] = std::move(locPtPoly[0]);
             minDistanceLocation[1] = std::move(locPtPoly[1]);
@@ -240,8 +240,8 @@ DistanceOp::computeContainmentDistance()
         computeInside(insideLocs1, polys0, locPtPoly);
         if(minDistance <= terminateDistance) {
             // flip locations, since we are testing geom 1 VS geom 0
-            assert(locPtPoly[0]);
-            assert(locPtPoly[1]);
+            assert(locPtPoly[0].getGeometryComponent());
+            assert(locPtPoly[1].getGeometryComponent());
 
             minDistanceLocation[0] = std::move(locPtPoly[1]);
             minDistanceLocation[1] = std::move(locPtPoly[0]);
@@ -254,18 +254,18 @@ DistanceOp::computeContainmentDistance()
 
 /*private*/
 void
-DistanceOp::computeInside(std::vector<std::unique_ptr<GeometryLocation>> & locs,
+DistanceOp::computeInside(std::vector<GeometryLocation> & locs,
                           const Polygon::ConstVect& polys,
-                          std::array<std::unique_ptr<GeometryLocation>, 2> & locPtPoly)
+                          std::array<GeometryLocation, 2> & locPtPoly)
 {
     for(auto& loc : locs) {
         for(const auto& poly : polys) {
-            const auto& pt = loc->getCoordinate();
+            const auto& pt = loc.getCoordinate();
 
 			if (Location::EXTERIOR != ptLocator.locate(pt, static_cast<const Geometry*>(poly))) {
 				minDistance = 0.0;
 				locPtPoly[0] = std::move(loc);
-				locPtPoly[1].reset(new GeometryLocation(poly, pt));
+                locPtPoly[1] = GeometryLocation(poly, pt);
 				return;
 			}
         }
@@ -279,7 +279,7 @@ DistanceOp::computeFacetDistance()
     using geom::util::LinearComponentExtracter;
     using geom::util::PointExtracter;
 
-    std::array<std::unique_ptr<GeometryLocation>, 2> locGeom;
+    std::array<GeometryLocation, 2> locGeom;
 
     /*
      * Geometries are not wholly inside, so compute distance from lines
@@ -314,8 +314,8 @@ DistanceOp::computeFacetDistance()
     std::cerr << "PointExtracter found " << pts1.size() << " points in geometry 2" << std::endl;
 #endif
 
-    locGeom[0] = nullptr;
-    locGeom[1] = nullptr;
+    locGeom[0] = GeometryLocation();
+    locGeom[1] = GeometryLocation();
     computeMinDistanceLinesPoints(lines0, pts1, locGeom);
     updateMinDistance(locGeom, false);
     if(minDistance <= terminateDistance) {
@@ -332,8 +332,8 @@ DistanceOp::computeFacetDistance()
     std::cerr << "PointExtracter found " << pts0.size() << " points in geometry 1" << std::endl;
 #endif
 
-    locGeom[0] = nullptr;
-    locGeom[1] = nullptr;
+    locGeom[0] = GeometryLocation();
+    locGeom[1] = GeometryLocation();
     computeMinDistanceLinesPoints(lines1, pts0, locGeom);
     updateMinDistance(locGeom, true);
     if(minDistance <= terminateDistance) {
@@ -343,8 +343,8 @@ DistanceOp::computeFacetDistance()
         return;
     }
 
-    locGeom[0] = nullptr;
-    locGeom[1] = nullptr;
+    locGeom[0] = GeometryLocation();
+    locGeom[1] = GeometryLocation();
     computeMinDistancePoints(pts0, pts1, locGeom);
     updateMinDistance(locGeom, false);
 
@@ -358,7 +358,7 @@ void
 DistanceOp::computeMinDistanceLines(
     const LineString::ConstVect& lines0,
     const LineString::ConstVect& lines1,
-    std::array<std::unique_ptr<GeometryLocation>, 2> & locGeom)
+    std::array<GeometryLocation, 2> & locGeom)
 {
     for(const LineString* line0 : lines0) {
         for(const LineString* line1 : lines1) {
@@ -379,7 +379,7 @@ void
 DistanceOp::computeMinDistancePoints(
     const Point::ConstVect& points0,
     const Point::ConstVect& points1,
-    std::array<std::unique_ptr<GeometryLocation>, 2> & locGeom)
+    std::array<GeometryLocation, 2> & locGeom)
 {
     for(const Point* pt0 : points0) {
         for(const Point* pt1 : points1) {
@@ -400,8 +400,8 @@ DistanceOp::computeMinDistancePoints(
             if(dist < minDistance) {
                 minDistance = dist;
                 // this is wrong - need to determine closest points on both segments!!!
-                locGeom[0].reset(new GeometryLocation(pt0, 0, *(pt0->getCoordinate())));
-                locGeom[1].reset(new GeometryLocation(pt1, 0, *(pt1->getCoordinate())));
+                locGeom[0] = GeometryLocation(pt0, 0, *(pt0->getCoordinate()));
+                locGeom[1] = GeometryLocation(pt1, 0, *(pt1->getCoordinate()));
             }
 
             if(minDistance <= terminateDistance) {
@@ -416,7 +416,7 @@ void
 DistanceOp::computeMinDistanceLinesPoints(
     const LineString::ConstVect& lines,
     const Point::ConstVect& points,
-    std::array<std::unique_ptr<GeometryLocation>, 2> & locGeom)
+    std::array<GeometryLocation, 2> & locGeom)
 {
     for(const LineString* line : lines) {
         for(const Point* pt : points) {
@@ -437,7 +437,7 @@ void
 DistanceOp::computeMinDistance(
     const LineString* line0,
     const LineString* line1,
-    std::array<std::unique_ptr<GeometryLocation>, 2> & locGeom)
+    std::array<GeometryLocation, 2> & locGeom)
 {
     using geos::algorithm::Distance;
 
@@ -484,8 +484,8 @@ DistanceOp::computeMinDistance(
                 LineSegment seg1{Coordinate(p10), Coordinate(p11)};
                 auto closestPt = seg0.closestPoints(seg1);
 
-                locGeom[0].reset(new GeometryLocation(line0, i, closestPt[0]));
-                locGeom[1].reset(new GeometryLocation(line1, j, closestPt[1]));
+                locGeom[0] = GeometryLocation(line0, i, closestPt[0]);
+                locGeom[1] = GeometryLocation(line1, j, closestPt[1]);
             }
             if(minDistance <= terminateDistance) {
                 return;
@@ -498,7 +498,7 @@ DistanceOp::computeMinDistance(
 void
 DistanceOp::computeMinDistance(const LineString* line,
                                const Point* pt,
-                               std::array<std::unique_ptr<GeometryLocation>, 2> & locGeom)
+                               std::array<GeometryLocation, 2> & locGeom)
 {
     using geos::algorithm::Distance;
 
@@ -524,8 +524,8 @@ DistanceOp::computeMinDistance(const LineString* line,
             Coordinate segClosestPoint;
             seg.closestPoint(*coord, segClosestPoint);
 
-            locGeom[0].reset(new GeometryLocation(line, i, segClosestPoint));
-            locGeom[1].reset(new GeometryLocation(pt, 0, *coord));
+            locGeom[0] = GeometryLocation(line, i, segClosestPoint);
+            locGeom[1] = GeometryLocation(pt, 0, *coord);
         }
         if(minDistance <= terminateDistance) {
             return;

commit 629c2f6939f30a4b19682a87915fb2457a90db3c
Author: Daniel Baston <dbaston at gmail.com>
Date:   Mon Apr 15 18:53:04 2024 -0400

    Extracter classes: add short circuits

diff --git a/src/geom/util/LinearComponentExtracter.cpp b/src/geom/util/LinearComponentExtracter.cpp
index 6217ff6f7..2dbc76444 100644
--- a/src/geom/util/LinearComponentExtracter.cpp
+++ b/src/geom/util/LinearComponentExtracter.cpp
@@ -32,6 +32,10 @@ LinearComponentExtracter::LinearComponentExtracter(std::vector<const LineString*
 void
 LinearComponentExtracter::getLines(const Geometry& geom, std::vector<const LineString*>& ret)
 {
+    if (geom.getDimension() == Dimension::P) {
+        return;
+    }
+
     LinearComponentExtracter lce(ret);
     geom.apply_ro(&lce);
 }
diff --git a/src/geom/util/PointExtracter.cpp b/src/geom/util/PointExtracter.cpp
index 79f615487..70375adc1 100644
--- a/src/geom/util/PointExtracter.cpp
+++ b/src/geom/util/PointExtracter.cpp
@@ -28,6 +28,10 @@ namespace util { // geos.geom.util
 void
 PointExtracter::getPoints(const Geometry& geom, Point::ConstVect& ret)
 {
+    if (!geom.hasDimension(Dimension::P)) {
+        return;
+    }
+
     PointExtracter pe(ret);
     geom.apply_ro(&pe);
 }
@@ -44,16 +48,16 @@ PointExtracter::PointExtracter(Point::ConstVect& newComps)
 void
 PointExtracter::filter_rw(Geometry* geom)
 {
-    if(const Point* p = dynamic_cast<const Point*>(geom)) {
-        comps.push_back(p);
+    if (geom->getGeometryTypeId() == GEOS_POINT) {
+        comps.push_back(static_cast<const Point*>(geom));
     }
 }
 
 void
 PointExtracter::filter_ro(const Geometry* geom)
 {
-    if(const Point* p = dynamic_cast<const Point*>(geom)) {
-        comps.push_back(p);
+    if (geom->getGeometryTypeId() == GEOS_POINT) {
+        comps.push_back(static_cast<const Point*>(geom));
     }
 }
 }
diff --git a/src/geom/util/PolygonExtracter.cpp b/src/geom/util/PolygonExtracter.cpp
index 5e4ae6bc6..879e913c5 100644
--- a/src/geom/util/PolygonExtracter.cpp
+++ b/src/geom/util/PolygonExtracter.cpp
@@ -27,6 +27,10 @@ namespace util { // geos.geom.util
 void
 PolygonExtracter::getPolygons(const Geometry& geom, std::vector<const Polygon*>& ret)
 {
+    if (!geom.hasDimension(Dimension::A)) {
+        return;
+    }
+
     PolygonExtracter pe(ret);
     geom.apply_ro(&pe);
 }

commit abc9dd178306cafad367e478e9a6e8ff9bb293f7
Author: Daniel Baston <dbaston at gmail.com>
Date:   Mon Apr 15 20:20:17 2024 -0400

    DistancePerfTest: Add Line-Point and Line-Line benchmarks

diff --git a/benchmarks/operation/DistancePerfTest.cpp b/benchmarks/operation/DistancePerfTest.cpp
index 7bdcb7134..e0f54ac1f 100644
--- a/benchmarks/operation/DistancePerfTest.cpp
+++ b/benchmarks/operation/DistancePerfTest.cpp
@@ -34,7 +34,47 @@ static void BM_PointPointDistance(benchmark::State& state) {
     }
 }
 
+static void BM_PointLineDistance(benchmark::State& state) {
+    geos::geom::Envelope e(-100, 100, -100, 100);
+
+    auto points = geos::benchmark::createPoints(e, 1000);
+
+    std::size_t points_per_line = 30;
+    std::size_t nlines = 100;
+    double line_size = e.getWidth() / static_cast<double>(nlines * nlines);
+
+    auto lines = geos::benchmark::createLines(e, nlines, line_size, points_per_line);
+
+    for (auto _ : state) {
+        for (const auto& line : lines) {
+            for (const auto& point : points) {
+                line->distance(point.get());
+            }
+        }
+    }
+}
+
+static void BM_LineLineDistance(benchmark::State& state) {
+    geos::geom::Envelope e(-100, 100, -100, 100);
+
+    std::size_t points_per_line = 30;
+    std::size_t nlines = 100;
+    double line_size = e.getWidth() / static_cast<double>(nlines * nlines);
+
+    auto lines = geos::benchmark::createLines(e, nlines, line_size, points_per_line);
+
+    for (auto _ : state) {
+        for (const auto& line1 : lines) {
+            for (const auto& line2 : lines) {
+                line1->distance(line2.get());
+            }
+        }
+    }
+}
+
 BENCHMARK(BM_PointPointDistance);
+BENCHMARK(BM_PointLineDistance);
+BENCHMARK(BM_LineLineDistance);
 
 BENCHMARK_MAIN();
 

commit ba59b3f38d962f8f0749657ca7a1b2ce74664097
Author: Dan Baston <dbaston at gmail.com>
Date:   Mon Apr 15 14:45:06 2024 -0400

    DistanceOp: Add short-circuit for point-point distance (#1067)

diff --git a/benchmarks/operation/CMakeLists.txt b/benchmarks/operation/CMakeLists.txt
index 10b0eb488..8df2a6bfd 100644
--- a/benchmarks/operation/CMakeLists.txt
+++ b/benchmarks/operation/CMakeLists.txt
@@ -10,3 +10,13 @@
 ################################################################################
 add_subdirectory(buffer)
 add_subdirectory(predicate)
+
+if (benchmark_FOUND)
+    add_executable(perf_distance DistancePerfTest.cpp)
+    target_include_directories(perf_distance PUBLIC
+            $<BUILD_INTERFACE:${PROJECT_SOURCE_DIR}/include>
+            $<BUILD_INTERFACE:${PROJECT_BINARY_DIR}/include>
+            $<BUILD_INTERFACE:${PROJECT_SOURCE_DIR}/benchmarks>)
+    target_link_libraries(perf_distance PRIVATE
+            benchmark::benchmark geos geos_cxx_flags)
+endif()
diff --git a/benchmarks/operation/DistancePerfTest.cpp b/benchmarks/operation/DistancePerfTest.cpp
new file mode 100644
index 000000000..7bdcb7134
--- /dev/null
+++ b/benchmarks/operation/DistancePerfTest.cpp
@@ -0,0 +1,40 @@
+/**********************************************************************
+ *
+ * GEOS - Geometry Engine Open Source
+ * http://geos.osgeo.org
+ *
+ * Copyright (C) 2024 Daniel Baston
+ *
+ * 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 <benchmark/benchmark.h>
+
+#include <BenchmarkUtils.h>
+#include <geos/geom/Coordinate.h>
+#include <geos/geom/Geometry.h>
+#include <geos/operation/distance/DistanceOp.h>
+
+using geos::operation::distance::DistanceOp;
+
+static void BM_PointPointDistance(benchmark::State& state) {
+    geos::geom::Envelope e(-100, 100, -100, 100);
+    auto points = geos::benchmark::createPoints(e, 100000);
+
+    for (auto _ : state) {
+        for (std::size_t i = 0; i < points.size(); i++) {
+            for (std::size_t j = 0; i < points.size(); i++) {
+                points[i]->distance(points[j].get());
+           }
+        }
+    }
+}
+
+BENCHMARK(BM_PointPointDistance);
+
+BENCHMARK_MAIN();
+
diff --git a/src/operation/distance/DistanceOp.cpp b/src/operation/distance/DistanceOp.cpp
index 952c68d96..fc19c31f7 100644
--- a/src/operation/distance/DistanceOp.cpp
+++ b/src/operation/distance/DistanceOp.cpp
@@ -110,6 +110,10 @@ DistanceOp::distance()
     if(geom[0]->isEmpty() || geom[1]->isEmpty()) {
         return 0.0;
     }
+    if(geom[0]->getGeometryTypeId() == GEOS_POINT && geom[1]->getGeometryTypeId() == GEOS_POINT) {
+        return static_cast<const Point*>(geom[0])->getCoordinate()->distance(*static_cast<const Point*>(geom[1])->getCoordinate());
+    }
+
     computeMinDistance();
     return minDistance;
 }

commit d9bee63471dea5596c5aaa89eaa0a8512f937773
Author: Daniel Baston <dbaston at gmail.com>
Date:   Fri Apr 12 12:49:25 2024 -0400

    Implement getEnvelopeInternal for curved types

diff --git a/include/geos/algorithm/CircularArcs.h b/include/geos/algorithm/CircularArcs.h
new file mode 100644
index 000000000..22293e548
--- /dev/null
+++ b/include/geos/algorithm/CircularArcs.h
@@ -0,0 +1,37 @@
+/**********************************************************************
+ *
+ * GEOS - Geometry Engine Open Source
+ * http://geos.osgeo.org
+ *
+ * Copyright (C) 2024 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/Coordinate.h>
+
+#include "geos/geom/Envelope.h"
+
+namespace geos {
+namespace algorithm {
+
+class GEOS_DLL CircularArcs {
+public:
+    /// Return the circle center of an arc defined by three points
+    static geom::CoordinateXY getCenter(const geom::CoordinateXY& p0, const geom::CoordinateXY& p1,
+                                        const geom::CoordinateXY& p2);
+
+    /// Expand an envelope to include an arc defined by three points
+    static void expandEnvelope(geom::Envelope& e, const geom::CoordinateXY& p0, const geom::CoordinateXY& p1,
+                               const geom::CoordinateXY& p2);
+};
+
+}
+}
diff --git a/include/geos/geom/CircularString.h b/include/geos/geom/CircularString.h
index e60009225..7d34cd9b2 100644
--- a/include/geos/geom/CircularString.h
+++ b/include/geos/geom/CircularString.h
@@ -56,6 +56,19 @@ public:
         return true;
     }
 
+protected:
+
+    /// \brief
+    /// Constructs a CircularString taking ownership the
+    /// given CoordinateSequence.
+    CircularString(std::unique_ptr<CoordinateSequence> && pts,
+                   const GeometryFactory& newFactory);
+
+    void geometryChangedAction() override
+    {
+        envelope = computeEnvelopeInternal(false);
+    }
+
 };
 
 
diff --git a/include/geos/geom/LineString.h b/include/geos/geom/LineString.h
index ebcaffc3d..faf021f20 100644
--- a/include/geos/geom/LineString.h
+++ b/include/geos/geom/LineString.h
@@ -120,6 +120,11 @@ protected:
         return SORTINDEX_LINESTRING;
     };
 
+    void geometryChangedAction() override
+    {
+        envelope = computeEnvelopeInternal(true);
+    }
+
 private:
 
     void validateConstruction();
diff --git a/include/geos/geom/SimpleCurve.h b/include/geos/geom/SimpleCurve.h
index 23c946742..187af0c91 100644
--- a/include/geos/geom/SimpleCurve.h
+++ b/include/geos/geom/SimpleCurve.h
@@ -126,14 +126,10 @@ protected:
     SimpleCurve(const SimpleCurve& other);
 
     SimpleCurve(std::unique_ptr<CoordinateSequence>&& newCoords,
+                bool isLinear,
                 const GeometryFactory& factory);
 
-    Envelope computeEnvelopeInternal() const;
-
-    void geometryChangedAction() override
-    {
-        envelope = computeEnvelopeInternal();
-    }
+    Envelope computeEnvelopeInternal(bool isLinear) const;
 
     // TODO: hold value or shared_ptr instead of unique_ptr
     std::unique_ptr<CoordinateSequence> points;
diff --git a/src/algorithm/CircularArcs.cpp b/src/algorithm/CircularArcs.cpp
new file mode 100644
index 000000000..abfd73f42
--- /dev/null
+++ b/src/algorithm/CircularArcs.cpp
@@ -0,0 +1,124 @@
+/**********************************************************************
+ *
+ * GEOS - Geometry Engine Open Source
+ * http://geos.osgeo.org
+ *
+ * Copyright (C) 2024 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/CircularArcs.h>
+
+#include "geos/algorithm/Angle.h"
+#include "geos/geom/Envelope.h"
+#include "geos/geom/Quadrant.h"
+
+using geos::geom::CoordinateXY;
+
+namespace geos {
+namespace algorithm {
+
+CoordinateXY
+CircularArcs::getCenter(const CoordinateXY& p0, const CoordinateXY& p1, const CoordinateXY& p2)
+{
+    if (p0.equals2D(p2)) {
+        // Closed circle
+        return { 0.5*(p0.x + p1.x), 0.5*(p0.y + p1.y) };
+    }
+
+    // Circumcenter formulas from Graphics Gems III
+    CoordinateXY a{p1.x - p2.x, p1.y - p2.y};
+    CoordinateXY b{p2.x - p0.x, p2.y - p0.y};
+    CoordinateXY c{p0.x - p1.x, p0.y - p1.y};
+
+    double d1 = -(b.x*c.x + b.y*c.y);
+    double d2 = -(c.x*a.x + c.y*a.y);
+    double d3 = -(a.x*b.x + a.y*b.y);
+
+    double e1 = d2*d3;
+    double e2 = d3*d1;
+    double e3 = d1*d2;
+    double e = e1 + e2 + e3;
+
+    CoordinateXY G3{p0.x + p1.x + p2.x, p0.y + p1.y + p2.y};
+    CoordinateXY H {(e1*p0.x + e2*p1.x + e3*p2.x) / e, (e1*p0.y + e2*p1.y + e3*p2.y) / e};
+
+    CoordinateXY center = {0.5*(G3.x - H.x), 0.5*(G3.y - H.y)};
+
+    return center;
+}
+
+void
+CircularArcs::expandEnvelope(geom::Envelope& e, const geom::CoordinateXY& p0, const geom::CoordinateXY& p1,
+                             const geom::CoordinateXY& p2)
+{
+    e.expandToInclude(p0);
+    e.expandToInclude(p1);
+    e.expandToInclude(p2);
+
+    CoordinateXY center = getCenter(p0, p1, p2);
+
+    // zero-length arc
+    if (center.equals2D(p0) || center.equals2D(p1)) {
+        return;
+    }
+
+    // collinear
+    if (std::isnan(center.x)) {
+        return;
+    }
+
+    auto orientation = Orientation::index(center, p0, p1);
+
+    //* 1 | 0
+    //* --+--
+    //* 2 | 3
+
+    using geom::Quadrant;
+
+    auto q0 = geom::Quadrant::quadrant(center, p0);
+    auto q2 = geom::Quadrant::quadrant(center, p2);
+    double R = center.distance(p1);
+
+    if (q0 == q2) {
+        // Start and end quadrants are the same. Either the arc crosses all of
+        // the axes, or none of the axes.
+        if (Orientation::index(center, p1, p2) != orientation) {
+            e.expandToInclude({center.x, center.y + R});
+            e.expandToInclude({center.x - R, center.y});
+            e.expandToInclude({center.x, center.y - R});
+            e.expandToInclude({center.x + R, center.y});
+        }
+
+        return;
+    }
+
+    if (orientation == Orientation::CLOCKWISE) {
+        std::swap(q0, q2);
+    }
+
+    for (auto q = q0 + 1; (q % 4) != ((q2+1) % 4); q++) {
+        switch (q % 4) {
+        case Quadrant::NW:
+            e.expandToInclude({center.x, center.y + R});
+            break;
+        case Quadrant::SW:
+            e.expandToInclude({center.x - R, center.y});
+            break;
+        case Quadrant::SE:
+            e.expandToInclude({center.x, center.y - R});
+            break;
+        case Quadrant::NE:
+            e.expandToInclude({center.x + R, center.y});
+            break;
+        }
+    }
+}
+
+}
+}
diff --git a/src/geom/CircularString.cpp b/src/geom/CircularString.cpp
index a4376fcb3..1bdaaf16d 100644
--- a/src/geom/CircularString.cpp
+++ b/src/geom/CircularString.cpp
@@ -20,6 +20,15 @@
 namespace geos {
 namespace geom {
 
+/*public*/
+CircularString::CircularString(std::unique_ptr<CoordinateSequence> && newCoords,
+                               const GeometryFactory& factory)
+    :
+    SimpleCurve(std::move(newCoords), false, factory)
+{
+    //validateConstruction();
+}
+
 CircularString::~CircularString() = default;
 
 std::string
diff --git a/src/geom/LineString.cpp b/src/geom/LineString.cpp
index f3a97822b..90e37479f 100644
--- a/src/geom/LineString.cpp
+++ b/src/geom/LineString.cpp
@@ -58,7 +58,7 @@ LineString::LineString(const LineString& ls)
 LineString::LineString(CoordinateSequence::Ptr && newCoords,
                        const GeometryFactory& factory)
     :
-    SimpleCurve(std::move(newCoords), factory)
+    SimpleCurve(std::move(newCoords), true, factory)
 {
     validateConstruction();
 }
diff --git a/src/geom/SimpleCurve.cpp b/src/geom/SimpleCurve.cpp
index 68c841435..a0e039760 100644
--- a/src/geom/SimpleCurve.cpp
+++ b/src/geom/SimpleCurve.cpp
@@ -17,6 +17,7 @@
 
 #include <geos/geom/SimpleCurve.h>
 
+#include <geos/algorithm/CircularArcs.h>
 #include <geos/algorithm/Orientation.h>
 #include <geos/geom/CoordinateFilter.h>
 #include <geos/geom/GeometryFactory.h>
@@ -38,21 +39,33 @@ SimpleCurve::SimpleCurve(const SimpleCurve& other)
 }
 
 SimpleCurve::SimpleCurve(std::unique_ptr<CoordinateSequence>&& newCoords,
+                         bool isLinear,
             const GeometryFactory& factory)
     : Curve(factory),
     points(newCoords ? std::move(newCoords) : std::make_unique<CoordinateSequence>()),
-    envelope(computeEnvelopeInternal())
+    envelope(computeEnvelopeInternal(isLinear))
 {
 }
 
 Envelope
-SimpleCurve::computeEnvelopeInternal() const
+SimpleCurve::computeEnvelopeInternal(bool isLinear) const
 {
     if(isEmpty()) {
         return Envelope();
     }
 
-    return points->getEnvelope();
+    if(isLinear) {
+        return points->getEnvelope();
+    } else {
+        Envelope e;
+        for (std::size_t i = 2; i < points->size(); i++) {
+            algorithm::CircularArcs::expandEnvelope(e,
+                                                    points->getAt<CoordinateXY>(i-2),
+                                                    points->getAt<CoordinateXY>(i-1),
+                                                    points->getAt<CoordinateXY>(i));
+        }
+        return e;
+    }
 }
 
 std::unique_ptr<CoordinateSequence>
diff --git a/tests/unit/algorithm/CircularArcsTest.cpp b/tests/unit/algorithm/CircularArcsTest.cpp
new file mode 100644
index 000000000..475261d4f
--- /dev/null
+++ b/tests/unit/algorithm/CircularArcsTest.cpp
@@ -0,0 +1,219 @@
+#include <tut/tut.hpp>
+
+#include <geos/geom/Coordinate.h>
+#include <geos/algorithm/CircularArcs.h>
+
+using geos::geom::CoordinateXY;
+using geos::algorithm::CircularArcs;
+using geos::geom::Envelope;
+
+namespace tut {
+
+struct test_circulararcs_data {
+    const double eps = 1e-8;
+
+    void checkEnvelope(const CoordinateXY& p0, const CoordinateXY& p1, const CoordinateXY& p2,
+                       double xmin, double ymin, double xmax, double ymax)
+    {
+        {
+            Envelope e;
+            CircularArcs::expandEnvelope(e, p0, p1, p2);
+
+            ensure_equals("p0-p1-p2 xmin", e.getMinX(), xmin, eps);
+            ensure_equals("p0-p1-p2 xmax", e.getMaxX(), xmax, eps);
+            ensure_equals("p0-p1-p2 ymin", e.getMinY(), ymin, eps);
+            ensure_equals("p0-p1-p2 ymax", e.getMaxY(), ymax, eps);
+        }
+
+        {
+            Envelope e;
+            CircularArcs::expandEnvelope(e, p2, p1, p0);
+
+            ensure_equals("p2-p1-p0 xmin", e.getMinX(), xmin, eps);
+            ensure_equals("p2-p1-p0 xmax", e.getMaxX(), xmax, eps);
+            ensure_equals("p2-p1-p0 ymin", e.getMinY(), ymin, eps);
+            ensure_equals("p2-p1-p0 ymax", e.getMaxY(), ymax, eps);
+        }
+    }
+};
+
+using group = test_group<test_circulararcs_data>;
+using object = group::object;
+
+group test_circulararcs_group("geos::algorithm::CircularArcs");
+
+template<>
+template<>
+void object::test<1>()
+{
+    CoordinateXY p0{0, 10};
+    CoordinateXY p1{100, 110};
+    CoordinateXY p2{200, 10};
+
+    auto center = CircularArcs::getCenter(p0, p1, p2);
+
+    ensure_equals(center, CoordinateXY{100, 10});
+}
+
+template<>
+template<>
+void object::test<2>()
+{
+    CoordinateXY p0{0, 0};
+    CoordinateXY p1{1, 1};
+    CoordinateXY p2{0, 2};
+
+    auto center = CircularArcs::getCenter(p0, p1, p2);
+
+    ensure_equals(center, CoordinateXY{0, 1});
+}
+
+template<>
+template<>
+void object::test<3>()
+{
+    CoordinateXY p0{54.22, 31.8};
+    CoordinateXY p1{16.07, 11.9};
+    CoordinateXY p2{12.22, 3.99};
+
+    auto center = CircularArcs::getCenter(p0, p1, p2);
+
+    ensure(center.distance(CoordinateXY{52.0123, -10.486}) < 1e-4);
+}
+
+// complete circle
+template<>
+template<>
+void object::test<4>()
+{
+    CoordinateXY p0{3, 4};
+    CoordinateXY p1{7, 8};
+    CoordinateXY p2{3, 4};
+
+    auto center = CircularArcs::getCenter(p0, p1, p2);
+
+    ensure_equals(center, CoordinateXY{5, 6});
+}
+
+// collinear
+template<>
+template<>
+void object::test<5>()
+{
+    CoordinateXY p0{1, 2};
+    CoordinateXY p1{2, 3};
+    CoordinateXY p2{3, 4};
+
+    auto center = CircularArcs::getCenter(p0, p1, p2);
+
+    ensure(std::isnan(center.x));
+    ensure(std::isnan(center.y));
+}
+
+// CCW quadrant 2 to quadrant 1
+template<>
+template<>
+void object::test<6>()
+{
+    CoordinateXY p0{-std::sqrt(2), -std::sqrt(2)};
+    CoordinateXY p1{2, 0};
+    CoordinateXY p2{-std::sqrt(2), std::sqrt(2)};
+
+    checkEnvelope(p0, p1, p2,
+                  -std::sqrt(2), -2, 2, 2);
+}
+
+// quadrant 0 to quadrant 0, crossing all axes
+template<>
+template<>
+void object::test<7>()
+{
+    CoordinateXY p0{std::sqrt(2), std::sqrt(2)};
+    CoordinateXY p1{2, 0};
+    CoordinateXY p2{std::sqrt(3), 1};
+
+    checkEnvelope(p0, p1, p2, -2, -2, 2, 2);
+}
+
+
+// quadrant 0 to quadrant 0, crossing no axes
+template<>
+template<>
+void object::test<8>()
+{
+    CoordinateXY p0{1, std::sqrt(3)};
+    CoordinateXY p1{std::sqrt(2), std::sqrt(2)};
+    CoordinateXY p2{std::sqrt(3), 1};
+
+    checkEnvelope(p0, p1, p2,
+                  1, 1, std::sqrt(3), std::sqrt(3));
+}
+
+// half circle with start points on -/+ x axis
+template<>
+template<>
+void object::test<9>()
+{
+    CoordinateXY p0{-1, 0};
+    CoordinateXY p1{0, 1};
+    CoordinateXY p2{1, 0};
+
+    checkEnvelope(p0, p1, p2,
+                  -1, 0, 1, 1);
+}
+
+// CCW quadrant 0 to quadrant 3
+template<>
+template<>
+void object::test<10>()
+{
+    CoordinateXY p0{std::sqrt(2), std::sqrt(2)};
+    CoordinateXY p1{-2, 0};
+    CoordinateXY p2{std::sqrt(2), -std::sqrt(2)};
+
+    checkEnvelope(p0, p1, p2,
+                  -2, -2, std::sqrt(2), 2);
+}
+
+// collinear
+template<>
+template<>
+void object::test<11>()
+{
+    CoordinateXY p0{-1, -1};
+    CoordinateXY p1{1, 1};
+    CoordinateXY p2{2, 2};
+
+    checkEnvelope(p0, p1, p2,
+                  -1, -1, 2, 2);
+}
+
+// collinear
+template<>
+template<>
+void object::test<12>()
+{
+
+    CoordinateXY p0{1, 2};
+    CoordinateXY p1{2, 3};
+    CoordinateXY p2{3, 4};
+
+    checkEnvelope(p0, p1, p2,
+                  1, 2, 3, 4);
+}
+
+// repeated
+template<>
+template<>
+void object::test<13>()
+{
+    CoordinateXY p0{3, 4};
+    CoordinateXY p1{3, 4};
+    CoordinateXY p2{3, 4};
+
+    checkEnvelope(p0, p1, p2,
+                  3, 4, 3, 4);
+}
+
+}
+
diff --git a/tests/unit/geom/CircularStringTest.cpp b/tests/unit/geom/CircularStringTest.cpp
index 6f5cf03ae..8e1eaf4f3 100644
--- a/tests/unit/geom/CircularStringTest.cpp
+++ b/tests/unit/geom/CircularStringTest.cpp
@@ -77,8 +77,8 @@ void object::test<2>()
     ensure_THROW(cs_->getLength(), geos::util::UnsupportedOperationException);
     ensure_equals("getNumGeometries", cs_->getNumGeometries(), 1u);
     ensure_equals("getNumPoints", cs_->getNumPoints(), 5u);
-    ensure(!cs_->getEnvelopeInternal()->isNull());
-    // FIXME calculate envelope correctly
+    geos::geom::Envelope expected(0, 4, -1, 1);
+    ensure("getEnvelopeInternal", cs_->getEnvelopeInternal()->equals(&expected));
 
     // Geometry dimension functions
     ensure_equals("getDimension", cs_->getDimension(), geos::geom::Dimension::L);
diff --git a/tests/unit/geom/CompoundCurveTest.cpp b/tests/unit/geom/CompoundCurveTest.cpp
index c394bf945..ae232f5ca 100644
--- a/tests/unit/geom/CompoundCurveTest.cpp
+++ b/tests/unit/geom/CompoundCurveTest.cpp
@@ -85,7 +85,8 @@ void object::test<2>()
     ensure_equals("getNumGeometries", cc_->getNumGeometries(), 1u);
     ensure_equals("getNumCurves", cc_->getNumCurves(), 2u);
     ensure_equals("getNumPoints", cc_->getNumPoints(), 5u); // FIXME should this be 5 or 4?
-    ensure(!cc_->getEnvelopeInternal()->isNull());
+    geos::geom::Envelope expected(0, 2, 0, 2);
+    ensure("getEnvelopeInternal", cc_->getEnvelopeInternal()->equals(&expected));
 
     // Geometry dimension functions
     ensure_equals("getDimension", cc_->getDimension(), geos::geom::Dimension::L);
diff --git a/tests/unit/geom/CurvePolygonTest.cpp b/tests/unit/geom/CurvePolygonTest.cpp
index 135f2dec3..0025235d9 100644
--- a/tests/unit/geom/CurvePolygonTest.cpp
+++ b/tests/unit/geom/CurvePolygonTest.cpp
@@ -95,7 +95,15 @@ void object::test<2>()
     ensure_equals("getNumGeometries", cp_->getNumGeometries(), 1u);
     ensure_equals("getNumPoints", cp_->getNumPoints(), 14u);
     ensure_equals("getNumInteriorRing", cp_->getNumInteriorRing(), 1u);
-    ensure(!cp_->getEnvelopeInternal()->isNull());
+    {
+        geos::geom::Envelope expected(0, 4, -0.618033988749895, 5);
+        const geos::geom::Envelope& actual = *cp_->getEnvelopeInternal();
+
+        ensure_equals("getEnvelopeInternal MinX", actual.getMinX(), expected.getMinX());
+        ensure_equals("getEnvelopeInternal MinY", actual.getMinY(), expected.getMinY());
+        ensure_equals("getEnvelopeInternal MaxX", actual.getMaxX(), expected.getMaxX());
+        ensure_equals("getEnvelopeInternal MaxY", actual.getMaxY(), expected.getMaxY());
+    }
 
     // Geometry dimension functions
     ensure_equals("getDimension", cp_->getDimension(), geos::geom::Dimension::A);

commit 479541dddcdc680b9e27d2c1d77c099488d05c5c
Author: Daniel Baston <dbaston at gmail.com>
Date:   Mon Mar 25 15:15:22 2024 -0400

    GeometryGraphOperation: Avoid leak when GeometryGraph ctor throws

diff --git a/include/geos/geomgraph/DirectedEdgeStar.h b/include/geos/geomgraph/DirectedEdgeStar.h
index 3e70ada97..15880ae73 100644
--- a/include/geos/geomgraph/DirectedEdgeStar.h
+++ b/include/geos/geomgraph/DirectedEdgeStar.h
@@ -21,6 +21,7 @@
 
 #pragma once
 
+#include <memory>
 #include <geos/export.h>
 #include <set>
 #include <string>
@@ -81,7 +82,7 @@ public:
     /** \brief
      * Compute the labelling for all dirEdges in this star, as well as the overall labelling
      */
-    void computeLabelling(std::vector<GeometryGraph*>* geom) override; // throw(TopologyException *);
+    void computeLabelling(const std::vector<std::unique_ptr<GeometryGraph>>&geom) override; // throw(TopologyException *);
 
     /** \brief
      * For each dirEdge in the star, merge the label from the sym dirEdge into the label
diff --git a/include/geos/geomgraph/EdgeEndStar.h b/include/geos/geomgraph/EdgeEndStar.h
index 2982703a8..40b20ad31 100644
--- a/include/geos/geomgraph/EdgeEndStar.h
+++ b/include/geos/geomgraph/EdgeEndStar.h
@@ -27,6 +27,7 @@
 #include <geos/geom/Coordinate.h>  // for p0,p1
 
 #include <array>
+#include <memory>
 #include <set>
 #include <string>
 #include <vector>
@@ -115,7 +116,7 @@ public:
 
     virtual EdgeEnd* getNextCW(EdgeEnd* ee);
 
-    virtual void computeLabelling(std::vector<GeometryGraph*>* geomGraph);
+    virtual void computeLabelling(const std::vector<std::unique_ptr<GeometryGraph>>&geomGraph);
     // throw(TopologyException *);
 
     virtual bool isAreaLabelsConsistent(const GeometryGraph& geomGraph);
@@ -148,8 +149,8 @@ protected:
 private:
 
     virtual geom::Location getLocation(uint32_t geomIndex,
-                                       const geom::Coordinate& p,
-                                       std::vector<GeometryGraph*>* geom);
+                                       const geom::Coordinate&p,
+                                       const std::vector<std::unique_ptr<GeometryGraph>>&geom);
 
     /** \brief
      * The location of the point for this star in
diff --git a/include/geos/operation/GeometryGraphOperation.h b/include/geos/operation/GeometryGraphOperation.h
index a088bccdf..0886e65b3 100644
--- a/include/geos/operation/GeometryGraphOperation.h
+++ b/include/geos/operation/GeometryGraphOperation.h
@@ -74,9 +74,13 @@ protected:
     /** \brief
      * The operation args into an array so they can be accessed by index
      */
-    std::vector<geomgraph::GeometryGraph*> arg;
+    std::vector<std::unique_ptr<geomgraph::GeometryGraph>> arg;
 
     void setComputationPrecision(const geom::PrecisionModel* pm);
+
+    // Declare type as noncopyable
+    GeometryGraphOperation(const GeometryGraphOperation& other) = delete;
+    GeometryGraphOperation& operator=(const GeometryGraphOperation& rhs) = delete;
 };
 
 } // namespace geos.operation
diff --git a/include/geos/operation/relate/RelateComputer.h b/include/geos/operation/relate/RelateComputer.h
index a232534d2..6a9d9288d 100644
--- a/include/geos/operation/relate/RelateComputer.h
+++ b/include/geos/operation/relate/RelateComputer.h
@@ -76,7 +76,7 @@ namespace relate { // geos::operation::relate
  */
 class GEOS_DLL RelateComputer {
 public:
-    RelateComputer(std::vector<geomgraph::GeometryGraph*>* newArg);
+    RelateComputer(std::vector<std::unique_ptr<geomgraph::GeometryGraph>>& newArg);
     ~RelateComputer() = default;
 
     std::unique_ptr<geom::IntersectionMatrix> computeIM();
@@ -87,7 +87,7 @@ private:
     algorithm::PointLocator ptLocator;
 
     /// the arg(s) of the operation
-    std::vector<geomgraph::GeometryGraph*>* arg;
+    const std::vector<std::unique_ptr<geomgraph::GeometryGraph>>& arg;
 
     geomgraph::NodeMap nodes;
 
diff --git a/src/geomgraph/DirectedEdgeStar.cpp b/src/geomgraph/DirectedEdgeStar.cpp
index 118c98ebe..44b0ded2a 100644
--- a/src/geomgraph/DirectedEdgeStar.cpp
+++ b/src/geomgraph/DirectedEdgeStar.cpp
@@ -130,7 +130,7 @@ DirectedEdgeStar::getRightmostEdge()
 
 /*public*/
 void
-DirectedEdgeStar::computeLabelling(std::vector<GeometryGraph*>* geom)
+DirectedEdgeStar::computeLabelling(const std::vector<std::unique_ptr<GeometryGraph>>&geom)
 //throw(TopologyException *)
 {
     // this call can throw a TopologyException
diff --git a/src/geomgraph/EdgeEndStar.cpp b/src/geomgraph/EdgeEndStar.cpp
index da3df4a2b..44fcc4f4b 100644
--- a/src/geomgraph/EdgeEndStar.cpp
+++ b/src/geomgraph/EdgeEndStar.cpp
@@ -91,10 +91,10 @@ EdgeEndStar::getNextCW(EdgeEnd* ee)
 
 /*public*/
 void
-EdgeEndStar::computeLabelling(std::vector<GeometryGraph*>* geomGraph)
+EdgeEndStar::computeLabelling(const std::vector<std::unique_ptr<GeometryGraph>>& geomGraph)
 //throw(TopologyException *)
 {
-    computeEdgeEndLabels((*geomGraph)[0]->getBoundaryNodeRule());
+    computeEdgeEndLabels(geomGraph[0]->getBoundaryNodeRule());
 
     // Propagate side labels  around the edges in the star
     // for each parent Geometry
@@ -185,12 +185,12 @@ EdgeEndStar::computeEdgeEndLabels(
 /*public*/
 Location
 EdgeEndStar::getLocation(uint32_t geomIndex,
-                         const Coordinate& p, std::vector<GeometryGraph*>* geom)
+                         const Coordinate& p, const std::vector<std::unique_ptr<GeometryGraph>>& geom)
 {
     // compute location only on demand
     if(ptInAreaLocation[geomIndex] == Location::NONE) {
         ptInAreaLocation[geomIndex] = algorithm::locate::SimplePointInAreaLocator::locate(p,
-                                      (*geom)[geomIndex]->getGeometry());
+                                      geom[geomIndex]->getGeometry());
     }
     return ptInAreaLocation[geomIndex];
 }
diff --git a/src/operation/GeometryGraphOperation.cpp b/src/operation/GeometryGraphOperation.cpp
index 42d157bd0..65b5c298c 100644
--- a/src/operation/GeometryGraphOperation.cpp
+++ b/src/operation/GeometryGraphOperation.cpp
@@ -54,9 +54,9 @@ GeometryGraphOperation::GeometryGraphOperation(const Geometry* g0,
         setComputationPrecision(pm1);
     }
 
-    arg[0] = new GeometryGraph(0, g0,
+    arg[0] = std::make_unique<GeometryGraph>(0, g0,
                                algorithm::BoundaryNodeRule::getBoundaryOGCSFS());
-    arg[1] = new GeometryGraph(1, g1,
+    arg[1] = std::make_unique<GeometryGraph>(1, g1,
                                algorithm::BoundaryNodeRule::getBoundaryOGCSFS());
 }
 
@@ -80,8 +80,8 @@ GeometryGraphOperation::GeometryGraphOperation(const Geometry* g0,
         setComputationPrecision(pm1);
     }
 
-    arg[0] = new GeometryGraph(0, g0, boundaryNodeRule);
-    arg[1] = new GeometryGraph(1, g1, boundaryNodeRule);
+    arg[0] = std::make_unique<GeometryGraph>(0, g0, boundaryNodeRule);
+    arg[1] = std::make_unique<GeometryGraph>(1, g1, boundaryNodeRule);
 }
 
 
@@ -93,7 +93,7 @@ GeometryGraphOperation::GeometryGraphOperation(const Geometry* g0):
 
     setComputationPrecision(pm0);
 
-    arg[0] = new GeometryGraph(0, g0);
+    arg[0] = std::make_unique<GeometryGraph>(0, g0);
 }
 
 const Geometry*
@@ -114,9 +114,6 @@ GeometryGraphOperation::setComputationPrecision(const PrecisionModel* pm)
 
 GeometryGraphOperation::~GeometryGraphOperation()
 {
-    for(unsigned int i = 0; i < arg.size(); ++i) {
-        delete arg[i];
-    }
 }
 
 } // namespace geos.operation
diff --git a/src/operation/relate/RelateComputer.cpp b/src/operation/relate/RelateComputer.cpp
index bbdd255fe..53f1bc627 100644
--- a/src/operation/relate/RelateComputer.cpp
+++ b/src/operation/relate/RelateComputer.cpp
@@ -61,7 +61,7 @@ namespace geos {
 namespace operation { // geos.operation
 namespace relate { // geos.operation.relate
 
-RelateComputer::RelateComputer(std::vector<GeometryGraph*>* newArg):
+RelateComputer::RelateComputer(std::vector<std::unique_ptr<GeometryGraph>>& newArg):
     arg(newArg),
     nodes(RelateNodeFactory::instance()),
     im(new IntersectionMatrix())
@@ -74,10 +74,10 @@ RelateComputer::computeIM()
     // since Geometries are finite and embedded in a 2-D space, the EE element must always be 2
     im->set(Location::EXTERIOR, Location::EXTERIOR, 2);
     // if the Geometries don't overlap there is nothing to do
-    const Envelope* e1 = (*arg)[0]->getGeometry()->getEnvelopeInternal();
-    const Envelope* e2 = (*arg)[1]->getGeometry()->getEnvelopeInternal();
+    const Envelope* e1 = arg[0]->getGeometry()->getEnvelopeInternal();
+    const Envelope* e2 = arg[1]->getGeometry()->getEnvelopeInternal();
     if(!e1->intersects(e2)) {
-        computeDisjointIM(im.get(), (*arg)[0]->getBoundaryNodeRule());
+        computeDisjointIM(im.get(), arg[0]->getBoundaryNodeRule());
         return std::move(im);
     }
 
@@ -88,7 +88,7 @@ RelateComputer::computeIM()
 #endif
 
     std::unique_ptr<SegmentIntersector> si1(
-        (*arg)[0]->computeSelfNodes(&li, false)
+        arg[0]->computeSelfNodes(&li, false)
     );
 
     GEOS_CHECK_FOR_INTERRUPTS();
@@ -100,7 +100,7 @@ RelateComputer::computeIM()
 #endif
 
     std::unique_ptr<SegmentIntersector> si2(
-        (*arg)[1]->computeSelfNodes(&li, false)
+        arg[1]->computeSelfNodes(&li, false)
     );
 
     GEOS_CHECK_FOR_INTERRUPTS();
@@ -113,7 +113,7 @@ RelateComputer::computeIM()
 
     // compute intersections between edges of the two input geometries
     std::unique_ptr< SegmentIntersector> intersector(
-        (*arg)[0]->computeEdgeIntersections((*arg)[1], &li, false)
+        arg[0]->computeEdgeIntersections(arg[1].get(), &li, false)
     );
 
     GEOS_CHECK_FOR_INTERRUPTS();
@@ -188,9 +188,9 @@ RelateComputer::computeIM()
      */
     // build EdgeEnds for all intersections
     EdgeEndBuilder eeBuilder;
-    auto&& ee0 = eeBuilder.computeEdgeEnds((*arg)[0]->getEdges());
+    auto&& ee0 = eeBuilder.computeEdgeEnds(arg[0]->getEdges());
     insertEdgeEnds(ee0);
-    auto&& ee1 = eeBuilder.computeEdgeEnds((*arg)[1]->getEdges());
+    auto&& ee1 = eeBuilder.computeEdgeEnds(arg[1]->getEdges());
 
 #if GEOS_DEBUG
     std::cerr << "RelateComputer::computeIM: "
@@ -246,8 +246,8 @@ void
 RelateComputer::computeProperIntersectionIM(SegmentIntersector* intersector, IntersectionMatrix* imX)
 {
     // If a proper intersection is found, we can set a lower bound on the IM.
-    int dimA = (*arg)[0]->getGeometry()->getDimension();
-    int dimB = (*arg)[1]->getGeometry()->getDimension();
+    int dimA = arg[0]->getGeometry()->getDimension();
+    int dimB = arg[1]->getGeometry()->getDimension();
     bool hasProper = intersector->hasProperIntersection();
     bool hasProperInterior = intersector->hasProperInteriorIntersection();
     // For Geometry's of dim 0 there can never be proper intersections.
@@ -311,7 +311,7 @@ RelateComputer::computeProperIntersectionIM(SegmentIntersector* intersector, Int
 void
 RelateComputer::copyNodesAndLabels(uint8_t argIndex)
 {
-    const NodeMap* nm = (*arg)[argIndex]->getNodeMap();
+    const NodeMap* nm = arg[argIndex]->getNodeMap();
     for(const auto& it: *nm) {
         const Node* graphNode = it.second.get();
         Node* newNode = nodes.addNode(graphNode->getCoordinate());
@@ -333,7 +333,7 @@ RelateComputer::copyNodesAndLabels(uint8_t argIndex)
 void
 RelateComputer::computeIntersectionNodes(uint8_t argIndex)
 {
-    std::vector<Edge*>* edges = (*arg)[argIndex]->getEdges();
+    std::vector<Edge*>* edges = arg[argIndex]->getEdges();
     for(Edge* e: *edges) {
         Location eLoc = e->getLabel().getLocation(argIndex);
         const EdgeIntersectionList& eiL = e->getEdgeIntersectionList();
@@ -361,7 +361,7 @@ RelateComputer::computeIntersectionNodes(uint8_t argIndex)
 void
 RelateComputer::labelIntersectionNodes(uint8_t argIndex)
 {
-    std::vector<Edge*>* edges = (*arg)[argIndex]->getEdges();
+    std::vector<Edge*>* edges = arg[argIndex]->getEdges();
     for(Edge* e: *edges) {
         Location eLoc = e->getLabel().getLocation(argIndex);
         EdgeIntersectionList& eiL = e->getEdgeIntersectionList();
@@ -384,12 +384,12 @@ RelateComputer::labelIntersectionNodes(uint8_t argIndex)
 void
 RelateComputer::computeDisjointIM(IntersectionMatrix* imX, const algorithm::BoundaryNodeRule& boundaryNodeRule)
 {
-    const Geometry* ga = (*arg)[0]->getGeometry();
+    const Geometry* ga = arg[0]->getGeometry();
     if(!ga->isEmpty()) {
         imX->set(Location::INTERIOR, Location::EXTERIOR, ga->getDimension());
         imX->set(Location::BOUNDARY, Location::EXTERIOR, getBoundaryDim(*ga, boundaryNodeRule));
     }
-    const Geometry* gb = (*arg)[1]->getGeometry();
+    const Geometry* gb = arg[1]->getGeometry();
     if(!gb->isEmpty()) {
         imX->set(Location::EXTERIOR, Location::INTERIOR, gb->getDimension());
         imX->set(Location::EXTERIOR, Location::BOUNDARY, getBoundaryDim(*gb, boundaryNodeRule));
@@ -453,10 +453,10 @@ RelateComputer::updateIM(IntersectionMatrix& imX)
 void
 RelateComputer::labelIsolatedEdges(uint8_t thisIndex, uint8_t targetIndex)
 {
-    std::vector<Edge*>* edges = (*arg)[thisIndex]->getEdges();
+    std::vector<Edge*>* edges = arg[thisIndex]->getEdges();
     for(Edge* e: *edges) {
         if(e->isIsolated()) {
-            labelIsolatedEdge(e, targetIndex, (*arg)[targetIndex]->getGeometry());
+            labelIsolatedEdge(e, targetIndex, arg[targetIndex]->getGeometry());
             isolatedEdges.push_back(e);
         }
     }
@@ -505,7 +505,7 @@ void
 RelateComputer::labelIsolatedNode(Node* n, uint8_t targetIndex)
 {
     Location loc = ptLocator.locate(n->getCoordinate(),
-                               (*arg)[targetIndex]->getGeometry());
+                               arg[targetIndex]->getGeometry());
     n->getLabel().setAllLocations(targetIndex, loc);
     //debugPrintln(n.getLabel());
 }
diff --git a/src/operation/relate/RelateOp.cpp b/src/operation/relate/RelateOp.cpp
index 719cddb7f..739b6d4cc 100644
--- a/src/operation/relate/RelateOp.cpp
+++ b/src/operation/relate/RelateOp.cpp
@@ -52,7 +52,7 @@ RelateOp::relate(const Geometry* a, const Geometry* b,
 
 RelateOp::RelateOp(const Geometry* g0, const Geometry* g1):
     GeometryGraphOperation(g0, g1),
-    relateComp(&arg)
+    relateComp(arg)
 {
 }
 
@@ -60,7 +60,7 @@ RelateOp::RelateOp(const Geometry* g0, const Geometry* g1,
                    const algorithm::BoundaryNodeRule& boundaryNodeRule)
     :
     GeometryGraphOperation(g0, g1, boundaryNodeRule),
-    relateComp(&arg)
+    relateComp(arg)
 {
 }
 

commit d12e2e68448b91d01bd7cfbdebaaee2f781c8a9a
Author: Daniel Baston <dbaston at gmail.com>
Date:   Mon Mar 25 10:00:10 2024 -0400

    Guard against curved inputs, add C API tests

diff --git a/capi/geos_c.h.in b/capi/geos_c.h.in
index d5dd9c425..246c54eee 100644
--- a/capi/geos_c.h.in
+++ b/capi/geos_c.h.in
@@ -210,7 +210,12 @@ enum GEOSGeomTypes {
     /** Multipolygon, a homogeneous collection of polygons */
     GEOS_MULTIPOLYGON,
     /** Geometry collection, a heterogeneous collection of geometry */
-    GEOS_GEOMETRYCOLLECTION
+    GEOS_GEOMETRYCOLLECTION,
+    GEOS_CIRCULARSTRING,
+    GEOS_COMPOUNDCURVE,
+    GEOS_CURVEPOLYGON,
+    GEOS_MULTICURVE,
+    GEOS_MULTISURFACE,
 };
 
 /**
diff --git a/capi/geos_ts_c.cpp b/capi/geos_ts_c.cpp
index 7109e0b72..6efc5f2f5 100644
--- a/capi/geos_ts_c.cpp
+++ b/capi/geos_ts_c.cpp
@@ -33,6 +33,7 @@
 #include <geos/coverage/CoverageUnion.h>
 #include <geos/geom/Coordinate.h>
 #include <geos/geom/CoordinateSequence.h>
+#include <geos/geom/Curve.h>
 #include <geos/geom/Envelope.h>
 #include <geos/geom/Geometry.h>
 #include <geos/geom/GeometryCollection.h>
@@ -41,12 +42,14 @@
 #include <geos/geom/LinearRing.h>
 #include <geos/geom/LineSegment.h>
 #include <geos/geom/LineString.h>
+#include <geos/geom/MultiCurve.h>
 #include <geos/geom/MultiLineString.h>
 #include <geos/geom/MultiPoint.h>
 #include <geos/geom/MultiPolygon.h>
 #include <geos/geom/Point.h>
 #include <geos/geom/Polygon.h>
 #include <geos/geom/PrecisionModel.h>
+#include <geos/geom/SimpleCurve.h>
 #include <geos/geom/prep/PreparedGeometry.h>
 #include <geos/geom/prep/PreparedGeometryFactory.h>
 #include <geos/geom/util/Densifier.h>
@@ -162,17 +165,21 @@ using geos::geom::CoordinateXY;
 using geos::geom::CoordinateXYM;
 using geos::geom::CoordinateXYZM;
 using geos::geom::CoordinateSequence;
+using geos::geom::Curve;
 using geos::geom::Envelope;
 using geos::geom::Geometry;
 using geos::geom::GeometryCollection;
 using geos::geom::GeometryFactory;
 using geos::geom::LineString;
 using geos::geom::LinearRing;
+using geos::geom::MultiCurve;
 using geos::geom::MultiLineString;
 using geos::geom::MultiPolygon;
 using geos::geom::Point;
 using geos::geom::Polygon;
 using geos::geom::PrecisionModel;
+using geos::geom::SimpleCurve;
+using geos::geom::Surface;
 
 using geos::io::WKTReader;
 using geos::io::WKTWriter;
@@ -1048,8 +1055,7 @@ extern "C" {
     GEOSisRing_r(GEOSContextHandle_t extHandle, const Geometry* g)
     {
         return execute(extHandle, 2, [&]() {
-            // both LineString* and LinearRing* can cast to LineString*
-            const LineString* ls = dynamic_cast<const LineString*>(g);
+            const Curve* ls = dynamic_cast<const Curve*>(g);
             if(ls) {
                 return ls->isRing();
             }
@@ -1692,6 +1698,8 @@ extern "C" {
                     if (g->getGeometryTypeId() == geos::geom::GeometryTypeId::GEOS_POLYGON) {
                         auto p = geos::detail::down_cast<Polygon*>(g);
                         p->orientRings(exteriorCW);
+                    } else if (g->getGeometryTypeId() == geos::geom::GeometryTypeId::GEOS_CURVEPOLYGON) {
+                        throw geos::util::UnsupportedOperationException("Curved geometries not supported.");
                     }
                 }
 
@@ -1710,9 +1718,9 @@ extern "C" {
     GEOSGetNumInteriorRings_r(GEOSContextHandle_t extHandle, const Geometry* g1)
     {
         return execute(extHandle, -1, [&]() {
-            const Polygon* p = dynamic_cast<const Polygon*>(g1);
+            const Surface* p = dynamic_cast<const Surface*>(g1);
             if(!p) {
-                throw IllegalArgumentException("Argument is not a Polygon");
+                throw IllegalArgumentException("Argument is not a Surface");
             }
             return static_cast<int>(p->getNumInteriorRing());
         });
@@ -1802,7 +1810,7 @@ extern "C" {
     GEOSisClosed_r(GEOSContextHandle_t extHandle, const Geometry* g1)
     {
         return execute(extHandle, 2, [&]() {
-            const LineString* ls = dynamic_cast<const LineString*>(g1);
+            const Curve* ls = dynamic_cast<const Curve*>(g1);
             if(ls) {
                 return ls->isClosed();
             }
@@ -1812,7 +1820,12 @@ extern "C" {
                 return mls->isClosed();
             }
 
-            throw IllegalArgumentException("Argument is not a LineString or MultiLineString");
+            const MultiCurve* mc = dynamic_cast<const MultiCurve*>(g1);
+            if(mc) {
+                return mc->isClosed();
+            }
+
+            throw IllegalArgumentException("Argument is not a Curve, MultiLineString, or MultiCurve");
         });
     }
 
@@ -1840,9 +1853,9 @@ extern "C" {
     GEOSGeomGetNumPoints_r(GEOSContextHandle_t extHandle, const Geometry* g1)
     {
         return execute(extHandle, -1, [&]() {
-            const LineString* ls = dynamic_cast<const LineString*>(g1);
+            const SimpleCurve* ls = dynamic_cast<const SimpleCurve*>(g1);
             if(!ls) {
-                throw IllegalArgumentException("Argument is not a LineString");
+                throw IllegalArgumentException("Argument is not a SimpleCurve");
             }
             return static_cast<int>(ls->getNumPoints());
         });
@@ -1926,9 +1939,9 @@ extern "C" {
     GEOSGetExteriorRing_r(GEOSContextHandle_t extHandle, const Geometry* g1)
     {
         return execute(extHandle, [&]() {
-            const Polygon* p = dynamic_cast<const Polygon*>(g1);
+            const Surface* p = dynamic_cast<const Surface*>(g1);
             if(!p) {
-                throw IllegalArgumentException("Invalid argument (must be a Polygon)");
+                throw IllegalArgumentException("Invalid argument (must be a Surface)");
             }
             return p->getExteriorRing();
         });
@@ -1942,9 +1955,9 @@ extern "C" {
     GEOSGetInteriorRingN_r(GEOSContextHandle_t extHandle, const Geometry* g1, int n)
     {
         return execute(extHandle, [&]() {
-            const Polygon* p = dynamic_cast<const Polygon*>(g1);
+            const Surface* p = dynamic_cast<const Surface*>(g1);
             if(!p) {
-                throw IllegalArgumentException("Invalid argument (must be a Polygon)");
+                throw IllegalArgumentException("Invalid argument (must be a Surface)");
             }
             if(n < 0) {
                 throw IllegalArgumentException("Index must be non-negative.");
@@ -2813,7 +2826,7 @@ extern "C" {
     GEOSGeom_getCoordSeq_r(GEOSContextHandle_t extHandle, const Geometry* g)
     {
         return execute(extHandle, [&]() {
-            const LineString* ls = dynamic_cast<const LineString*>(g);
+            const SimpleCurve* ls = dynamic_cast<const SimpleCurve*>(g);
             if(ls) {
                 return ls->getCoordinatesRO();
             }
diff --git a/include/geos/algorithm/hull/ConcaveHull.h b/include/geos/algorithm/hull/ConcaveHull.h
index 1420e1f0a..6201e21b5 100644
--- a/include/geos/algorithm/hull/ConcaveHull.h
+++ b/include/geos/algorithm/hull/ConcaveHull.h
@@ -96,15 +96,7 @@ class GEOS_DLL ConcaveHull {
 
 public:
 
-    ConcaveHull(const Geometry* geom)
-        : inputGeometry(geom)
-        , maxEdgeLengthRatio(-1.0)
-        , alpha(-1)
-        , isHolesAllowed(false)
-        , criteriaType(PARAM_EDGE_LENGTH)
-        , maxSizeInHull(0.0)
-        , geomFactory(geom->getFactory())
-        {};
+    ConcaveHull(const Geometry* geom);
 
     /**
     * Computes the approximate edge length of
diff --git a/include/geos/geom/CircularString.h b/include/geos/geom/CircularString.h
index 5a8734954..e60009225 100644
--- a/include/geos/geom/CircularString.h
+++ b/include/geos/geom/CircularString.h
@@ -52,6 +52,10 @@ public:
         throw util::UnsupportedOperationException("Cannot calculate length of CircularString");
     }
 
+    bool hasCurvedComponents() const override {
+        return true;
+    }
+
 };
 
 
diff --git a/include/geos/geom/CompoundCurve.h b/include/geos/geom/CompoundCurve.h
index 7228a144b..253e7dce2 100644
--- a/include/geos/geom/CompoundCurve.h
+++ b/include/geos/geom/CompoundCurve.h
@@ -92,6 +92,7 @@ public:
 
     int compareToSameClass(const Geometry* geom) const override;
 
+    bool hasCurvedComponents() const override;
 
 protected:
     CompoundCurve(std::vector<std::unique_ptr<SimpleCurve>>&&,
diff --git a/include/geos/geom/Curve.h b/include/geos/geom/Curve.h
index 520a4c299..a8367c817 100644
--- a/include/geos/geom/Curve.h
+++ b/include/geos/geom/Curve.h
@@ -36,6 +36,8 @@ public:
 
     virtual bool isClosed() const = 0;
 
+    bool isRing() const;
+
     using Geometry::apply_ro;
     using Geometry::apply_rw;
 
diff --git a/include/geos/geom/CurvePolygon.h b/include/geos/geom/CurvePolygon.h
index 9300df97f..a030fc66b 100644
--- a/include/geos/geom/CurvePolygon.h
+++ b/include/geos/geom/CurvePolygon.h
@@ -43,6 +43,8 @@ public:
 
     double getArea() const override;
 
+    bool hasCurvedComponents() const override;
+
 protected:
     using SurfaceImpl::SurfaceImpl;
 
diff --git a/include/geos/geom/Geometry.h b/include/geos/geom/Geometry.h
index 488332c1c..ead07c1b9 100644
--- a/include/geos/geom/Geometry.h
+++ b/include/geos/geom/Geometry.h
@@ -310,10 +310,14 @@ public:
     virtual std::string getGeometryType() const = 0; //Abstract
 
     /// Returns whether the Geometry type _may_ contain curved elements
-    virtual bool isCurvedType() const;
+    /// FIXME: this would be true for GeometryCollection ?
+    bool isCurvedType() const;
 
     static bool isCurvedType(GeometryTypeId);
 
+    /// Returns whether the Geometry contains curved components
+    virtual bool hasCurvedComponents() const;
+
     /// Return an integer representation of this Geometry type
     virtual GeometryTypeId getGeometryTypeId() const = 0; //Abstract
 
diff --git a/include/geos/geom/GeometryCollection.h b/include/geos/geom/GeometryCollection.h
index 7e0428c1f..ae9a441aa 100644
--- a/include/geos/geom/GeometryCollection.h
+++ b/include/geos/geom/GeometryCollection.h
@@ -246,6 +246,8 @@ protected:
 
     int compareToSameClass(const Geometry* gc) const override;
 
+    bool hasCurvedComponents() const override;
+
 };
 
 } // namespace geos::geom
diff --git a/include/geos/geom/MultiLineString.h b/include/geos/geom/MultiLineString.h
index 47f2abaf9..2b6cc76fd 100644
--- a/include/geos/geom/MultiLineString.h
+++ b/include/geos/geom/MultiLineString.h
@@ -138,6 +138,11 @@ protected:
         return SORTINDEX_MULTILINESTRING;
     };
 
+    bool hasCurvedComponents() const override
+    {
+        return false;
+    }
+
 };
 
 
diff --git a/include/geos/geom/MultiPoint.h b/include/geos/geom/MultiPoint.h
index 84863681e..0e1d564d1 100644
--- a/include/geos/geom/MultiPoint.h
+++ b/include/geos/geom/MultiPoint.h
@@ -134,6 +134,11 @@ protected:
         return SORTINDEX_MULTIPOINT;
     };
 
+    bool hasCurvedComponents() const override
+    {
+        return false;
+    }
+
 };
 
 #ifdef _MSC_VER
diff --git a/include/geos/geom/MultiPolygon.h b/include/geos/geom/MultiPolygon.h
index ab5b27d50..f4cca3a38 100644
--- a/include/geos/geom/MultiPolygon.h
+++ b/include/geos/geom/MultiPolygon.h
@@ -140,6 +140,11 @@ protected:
         return SORTINDEX_MULTIPOLYGON;
     };
 
+    bool hasCurvedComponents() const override
+    {
+        return false;
+    }
+
 };
 
 #ifdef _MSC_VER
diff --git a/include/geos/geom/SimpleCurve.h b/include/geos/geom/SimpleCurve.h
index 240b54170..23c946742 100644
--- a/include/geos/geom/SimpleCurve.h
+++ b/include/geos/geom/SimpleCurve.h
@@ -83,8 +83,6 @@ public:
 
     bool isClosed() const override;
 
-    bool isRing() const;
-
     virtual bool isCoordinate(CoordinateXY& pt) const;
 
     bool equalsExact(const Geometry* other, double tolerance = 0)
diff --git a/include/geos/operation/polygonize/Polygonizer.h b/include/geos/operation/polygonize/Polygonizer.h
index 50fce1f7c..1c21a3a34 100644
--- a/include/geos/operation/polygonize/Polygonizer.h
+++ b/include/geos/operation/polygonize/Polygonizer.h
@@ -192,16 +192,6 @@ public:
      */
     void add(std::vector<const geom::Geometry*>* geomList);
 
-    /**
-     * Add a geometry to the linework to be polygonized.
-     * May be called multiple times.
-     * Any dimension of Geometry may be added;
-     * the constituent linework will be extracted and used
-     *
-     * @param g a Geometry with linework to be polygonized
-     */
-    void add(geom::Geometry* g);
-
     /**
      * Add a geometry to the linework to be polygonized.
      * May be called multiple times.
diff --git a/include/geos/triangulate/polygon/ConstrainedDelaunayTriangulator.h b/include/geos/triangulate/polygon/ConstrainedDelaunayTriangulator.h
index c8cc8ac33..23c385d21 100644
--- a/include/geos/triangulate/polygon/ConstrainedDelaunayTriangulator.h
+++ b/include/geos/triangulate/polygon/ConstrainedDelaunayTriangulator.h
@@ -55,7 +55,7 @@ private:
     const Geometry* inputGeom;
     const GeometryFactory* geomFact;
 
-    std::unique_ptr<Geometry> compute();
+    std::unique_ptr<Geometry> compute() const;
 
     static std::unique_ptr<Geometry> toGeometry(
         const geom::GeometryFactory* geomFact,
diff --git a/include/geos/util.h b/include/geos/util.h
index bd2ff674d..04405b1e2 100644
--- a/include/geos/util.h
+++ b/include/geos/util.h
@@ -66,7 +66,7 @@ namespace util {
 template<typename T>
 void ensureNotCurvedType(const T* geom)
 {
-    if (geom->isCurvedType()) {
+    if (geom->hasCurvedComponents()) {
         throw UnsupportedOperationException("Curved geometry types are not supported.");
     }
 }
@@ -74,7 +74,7 @@ void ensureNotCurvedType(const T* geom)
 template<typename T>
 void ensureNotCurvedType(const T& geom)
 {
-    if (geom.isCurvedType()) {
+    if (geom.hasCurvedComponents()) {
         throw UnsupportedOperationException("Curved geometry types are not supported.");
     }
 }
diff --git a/src/algorithm/distance/DiscreteFrechetDistance.cpp b/src/algorithm/distance/DiscreteFrechetDistance.cpp
index a54dd8146..03934def7 100644
--- a/src/algorithm/distance/DiscreteFrechetDistance.cpp
+++ b/src/algorithm/distance/DiscreteFrechetDistance.cpp
@@ -28,6 +28,8 @@
 #include <algorithm>
 #include <limits>
 #include <iostream>
+
+#include "geos/util.h"
 using namespace geos::geom;
 
 namespace geos {
@@ -142,6 +144,9 @@ DiscreteFrechetDistance::compute(
         throw util::IllegalArgumentException("DiscreteFrechetDistance called with empty inputs.");
     }
 
+    util::ensureNotCurvedType(discreteGeom);
+    util::ensureNotCurvedType(geom);
+
     auto lp = discreteGeom.getCoordinates();
     auto lq = geom.getCoordinates();
     std::size_t pSize, qSize;
diff --git a/src/algorithm/distance/DiscreteHausdorffDistance.cpp b/src/algorithm/distance/DiscreteHausdorffDistance.cpp
index cffc7b6a7..a02f0a661 100644
--- a/src/algorithm/distance/DiscreteHausdorffDistance.cpp
+++ b/src/algorithm/distance/DiscreteHausdorffDistance.cpp
@@ -23,6 +23,8 @@
 #include <cassert>
 #include <limits>
 
+#include "geos/util.h"
+
 using namespace geos::geom;
 
 namespace geos {
@@ -101,6 +103,9 @@ DiscreteHausdorffDistance::computeOrientedDistance(
     const geom::Geometry& geom,
     PointPairDistance& p_ptDist)
 {
+    util::ensureNotCurvedType(discreteGeom);
+    util::ensureNotCurvedType(geom);
+
     // can't calculate distance with empty
     if (discreteGeom.isEmpty() || geom.isEmpty()) return;
 
diff --git a/src/algorithm/hull/ConcaveHull.cpp b/src/algorithm/hull/ConcaveHull.cpp
index bbee04bad..927008862 100644
--- a/src/algorithm/hull/ConcaveHull.cpp
+++ b/src/algorithm/hull/ConcaveHull.cpp
@@ -28,6 +28,7 @@
 #include <geos/util/IllegalArgumentException.h>
 #include <geos/util/IllegalStateException.h>
 #include <geos/util/Assert.h>
+#include <geos/util.h>
 
 
 using geos::geom::Coordinate;
@@ -48,6 +49,17 @@ namespace geos {
 namespace algorithm { // geos.algorithm
 namespace hull {      // geos.algorithm.hulll
 
+ConcaveHull::ConcaveHull(const Geometry* geom)
+    : inputGeometry(geom)
+    , maxEdgeLengthRatio(-1.0)
+    , alpha(-1)
+    , isHolesAllowed(false)
+    , criteriaType(PARAM_EDGE_LENGTH)
+    , maxSizeInHull(0.0)
+    , geomFactory(geom->getFactory())
+{
+    util::ensureNotCurvedType(geom);
+}
 
 /* public static */
 double
diff --git a/src/algorithm/hull/ConcaveHullOfPolygons.cpp b/src/algorithm/hull/ConcaveHullOfPolygons.cpp
index a774cf6f0..e26176254 100644
--- a/src/algorithm/hull/ConcaveHullOfPolygons.cpp
+++ b/src/algorithm/hull/ConcaveHullOfPolygons.cpp
@@ -26,6 +26,8 @@
 #include <geos/triangulate/tri/Tri.h>
 #include <geos/util/IllegalArgumentException.h>
 
+#include "geos/util.h"
+
 
 using geos::geom::Coordinate;
 using geos::geom::Geometry;
@@ -111,6 +113,7 @@ ConcaveHullOfPolygons::ConcaveHullOfPolygons(const Geometry* geom)
     , isHolesAllowed(false)
     , isTight(false)
 {
+    util::ensureNotCurvedType(geom);
     if (! geom->isPolygonal()) {
         throw util::IllegalArgumentException("Input must be polygonal");
     }
diff --git a/src/coverage/CoverageRingEdges.cpp b/src/coverage/CoverageRingEdges.cpp
index bc6c34e9b..fde884f28 100644
--- a/src/coverage/CoverageRingEdges.cpp
+++ b/src/coverage/CoverageRingEdges.cpp
@@ -71,6 +71,8 @@ CoverageRingEdges::build()
     std::map<LineSegment, CoverageEdge*> uniqueEdgeMap;
     for (const Geometry* geom : m_coverage) {
         for (std::size_t ipoly = 0; ipoly < geom->getNumGeometries(); ipoly++) {
+            util::ensureNotCurvedType(geom->getGeometryN(ipoly));
+
             const Polygon* poly = static_cast<const Polygon*>(geom->getGeometryN(ipoly));
 
             //-- skip empty elements. Missing elements are copied in result
diff --git a/src/coverage/CoverageValidator.cpp b/src/coverage/CoverageValidator.cpp
index 719c7f134..fedad7b49 100644
--- a/src/coverage/CoverageValidator.cpp
+++ b/src/coverage/CoverageValidator.cpp
@@ -74,6 +74,8 @@ CoverageValidator::validate()
     TemplateSTRtree<const Geometry*> index;
     std::vector<std::unique_ptr<Geometry>> invalidLines;
     for (auto* geom : m_coverage) {
+        util::ensureNotCurvedType(geom);
+
         index.insert(geom);
         invalidLines.emplace_back(nullptr);
     }
diff --git a/src/geom/CompoundCurve.cpp b/src/geom/CompoundCurve.cpp
index a5aa22552..f44f54876 100644
--- a/src/geom/CompoundCurve.cpp
+++ b/src/geom/CompoundCurve.cpp
@@ -253,6 +253,15 @@ CompoundCurve::compareToSameClass(const Geometry* g) const
     return compare(curves, curve->curves);
 }
 
+bool CompoundCurve::hasCurvedComponents() const {
+    for (const auto& curve : curves) {
+        if (curve->hasCurvedComponents()) {
+            return true;
+        }
+    }
+    return false;
+}
+
 void
 CompoundCurve::normalize()
 {
diff --git a/src/geom/Curve.cpp b/src/geom/Curve.cpp
index fd3013ec5..4bd3d0244 100644
--- a/src/geom/Curve.cpp
+++ b/src/geom/Curve.cpp
@@ -48,5 +48,12 @@ Curve::apply_ro(GeometryComponentFilter* filter) const
     filter->filter_ro(this);
 }
 
+bool
+Curve::isRing() const
+{
+    return isClosed() && isSimple();
+}
+
+
 }
 }
\ No newline at end of file
diff --git a/src/geom/CurvePolygon.cpp b/src/geom/CurvePolygon.cpp
index 7afb1cb04..74a5e943b 100644
--- a/src/geom/CurvePolygon.cpp
+++ b/src/geom/CurvePolygon.cpp
@@ -56,6 +56,18 @@ namespace geom {
         throw util::UnsupportedOperationException();
     }
 
+    bool CurvePolygon::hasCurvedComponents() const {
+        if (shell->hasCurvedComponents()) {
+            return true;
+        }
+        for (const auto& hole : holes) {
+            if (hole->hasCurvedComponents()) {
+                return true;
+            }
+        }
+        return false;
+    }
+
     Geometry*
     CurvePolygon::cloneImpl() const {
         return new CurvePolygon(*this);
diff --git a/src/geom/Geometry.cpp b/src/geom/Geometry.cpp
index c6c6f766d..6c5b43c3b 100644
--- a/src/geom/Geometry.cpp
+++ b/src/geom/Geometry.cpp
@@ -205,6 +205,8 @@ Geometry::getCentroid(CoordinateXY& ret) const
 std::unique_ptr<Point>
 Geometry::getInteriorPoint() const
 {
+    geos::util::ensureNotCurvedType(this);
+
     Coordinate interiorPt;
     int dim = getDimension();
     if(dim == 0) {
@@ -815,6 +817,11 @@ Geometry::isCurvedType() const {
     return isCurvedType(getGeometryTypeId());
 }
 
+bool
+Geometry::hasCurvedComponents() const {
+    return false;
+}
+
 } // namespace geos::geom
 } // namespace geos
 
diff --git a/src/geom/GeometryCollection.cpp b/src/geom/GeometryCollection.cpp
index 107b3b8bd..096ca3526 100644
--- a/src/geom/GeometryCollection.cpp
+++ b/src/geom/GeometryCollection.cpp
@@ -333,6 +333,15 @@ GeometryCollection::compareToSameClass(const Geometry* g) const
     return compare(geometries, gc->geometries);
 }
 
+bool GeometryCollection::hasCurvedComponents() const {
+    for (const auto& g : geometries) {
+        if (g->hasCurvedComponents()) {
+            return true;
+        }
+    }
+    return false;
+}
+
 const CoordinateXY*
 GeometryCollection::getCoordinate() const
 {
diff --git a/src/geom/SimpleCurve.cpp b/src/geom/SimpleCurve.cpp
index 471d48ff8..68c841435 100644
--- a/src/geom/SimpleCurve.cpp
+++ b/src/geom/SimpleCurve.cpp
@@ -165,12 +165,6 @@ SimpleCurve::isClosed() const
     return points->front<CoordinateXY>().equals2D(points->back<CoordinateXY>());
 }
 
-bool
-SimpleCurve::isRing() const
-{
-    return isClosed() && isSimple();
-}
-
 std::unique_ptr<Geometry>
 SimpleCurve::getBoundary() const
 {
diff --git a/src/geom/prep/PreparedLineString.cpp b/src/geom/prep/PreparedLineString.cpp
index 819a42d6c..728490571 100644
--- a/src/geom/prep/PreparedLineString.cpp
+++ b/src/geom/prep/PreparedLineString.cpp
@@ -57,6 +57,8 @@ PreparedLineString::getIntersectionFinder()
 bool
 PreparedLineString::intersects(const geom::Geometry* g) const
 {
+    geos::util::ensureNotCurvedType(g);
+
     if(! envelopesIntersect(g)) {
         return false;
     }
diff --git a/src/geom/prep/PreparedPoint.cpp b/src/geom/prep/PreparedPoint.cpp
index 8fa3f94f5..88aac78b3 100644
--- a/src/geom/prep/PreparedPoint.cpp
+++ b/src/geom/prep/PreparedPoint.cpp
@@ -17,8 +17,11 @@
  **********************************************************************/
 
 
+#include <geos/geom/Geometry.h>
 #include <geos/geom/prep/PreparedPoint.h>
 
+#include "geos/util.h"
+
 namespace geos {
 namespace geom { // geos.geom
 namespace prep { // geos.geom.prep
@@ -26,6 +29,8 @@ namespace prep { // geos.geom.prep
 bool
 PreparedPoint::intersects(const geom::Geometry* g) const
 {
+    util::ensureNotCurvedType(g);
+
     if(! envelopesIntersect(g)) {
         return false;
     }
diff --git a/src/geom/prep/PreparedPolygon.cpp b/src/geom/prep/PreparedPolygon.cpp
index 332c4798e..c77b828a3 100644
--- a/src/geom/prep/PreparedPolygon.cpp
+++ b/src/geom/prep/PreparedPolygon.cpp
@@ -136,6 +136,8 @@ bool
 PreparedPolygon::
 intersects(const geom::Geometry* g) const
 {
+    geos::util::ensureNotCurvedType(g);
+
     // envelope test
     if(!envelopesIntersect(g)) {
         return false;
diff --git a/src/geom/util/GeometryEditor.cpp b/src/geom/util/GeometryEditor.cpp
index b2d66b947..bbeaf98fb 100644
--- a/src/geom/util/GeometryEditor.cpp
+++ b/src/geom/util/GeometryEditor.cpp
@@ -74,6 +74,8 @@ GeometryEditor::GeometryEditor(const GeometryFactory* newFactory)
 std::unique_ptr<Geometry>
 GeometryEditor::edit(const Geometry* geometry, GeometryEditorOperation* operation)
 {
+    geos::util::ensureNotCurvedType(geometry);
+
     // if client did not supply a GeometryFactory, use the one from the input Geometry
     if(factory == nullptr) {
         factory = geometry->getFactory();
diff --git a/src/geomgraph/GeometryGraph.cpp b/src/geomgraph/GeometryGraph.cpp
index b1b2e53f1..5f47861ae 100644
--- a/src/geomgraph/GeometryGraph.cpp
+++ b/src/geomgraph/GeometryGraph.cpp
@@ -169,6 +169,8 @@ void
 GeometryGraph::add(const Geometry* g)
 //throw (UnsupportedOperationException *)
 {
+    util::ensureNotCurvedType(g);
+
     if(g->isEmpty()) {
         return;
     }
diff --git a/src/io/GeoJSONWriter.cpp b/src/io/GeoJSONWriter.cpp
index 154b8266d..f17455105 100644
--- a/src/io/GeoJSONWriter.cpp
+++ b/src/io/GeoJSONWriter.cpp
@@ -30,6 +30,8 @@
 #include <sstream>
 #include <cassert>
 
+#include "geos/util.h"
+
 #define GEOS_COMPILATION
 
 using namespace geos::geom;
@@ -174,6 +176,8 @@ void GeoJSONWriter::encodeFeatureCollection(const geom::Geometry* g, geos_nlohma
 
 void GeoJSONWriter::encodeGeometry(const geom::Geometry* geometry, geos_nlohmann::ordered_json& j)
 {
+    util::ensureNotCurvedType(geometry);
+
     auto type = geometry->getGeometryTypeId();
     if (type == GEOS_POINT) {
         auto point = static_cast<const geom::Point*>(geometry);
diff --git a/src/io/WKBWriter.cpp b/src/io/WKBWriter.cpp
index 43ad801a0..dd82b58b6 100644
--- a/src/io/WKBWriter.cpp
+++ b/src/io/WKBWriter.cpp
@@ -36,6 +36,8 @@
 #include <sstream>
 #include <cassert>
 
+#include "geos/util.h"
+
 #undef DEBUG_WKB_WRITER
 
 
@@ -97,6 +99,8 @@ WKBWriter::writeHEX(const Geometry& g, std::ostream& os)
 void
 WKBWriter::write(const Geometry& g, std::ostream& os)
 {
+    util::ensureNotCurvedType(g);
+
     OrdinateSet inputOrdinates = OrdinateSet::createXY();
     inputOrdinates.setM(g.hasM());
     inputOrdinates.setZ(g.hasZ());
diff --git a/src/noding/GeometryNoder.cpp b/src/noding/GeometryNoder.cpp
index 144d1f306..65a7c9941 100644
--- a/src/noding/GeometryNoder.cpp
+++ b/src/noding/GeometryNoder.cpp
@@ -92,6 +92,7 @@ GeometryNoder::GeometryNoder(const geom::Geometry& g)
     :
     argGeom(g)
 {
+    util::ensureNotCurvedType(argGeom);
 }
 
 /* private */
diff --git a/src/operation/linemerge/LineMerger.cpp b/src/operation/linemerge/LineMerger.cpp
index 8163c9a62..1f90ca4ac 100644
--- a/src/operation/linemerge/LineMerger.cpp
+++ b/src/operation/linemerge/LineMerger.cpp
@@ -94,6 +94,8 @@ struct LMGeometryComponentFilter: public GeometryComponentFilter {
 void
 LineMerger::add(const Geometry* geometry)
 {
+    util::ensureNotCurvedType(geometry);
+
     LMGeometryComponentFilter lmgcf(this);
     geometry->apply_ro(&lmgcf);
 }
diff --git a/src/operation/polygonize/Polygonizer.cpp b/src/operation/polygonize/Polygonizer.cpp
index 1eb524149..cc8abccb0 100644
--- a/src/operation/polygonize/Polygonizer.cpp
+++ b/src/operation/polygonize/Polygonizer.cpp
@@ -103,20 +103,6 @@ Polygonizer::add(std::vector<const Geometry*>* geomList)
     }
 }
 
-/*
- * Add a geometry to the linework to be polygonized.
- * May be called multiple times.
- * Any dimension of Geometry may be added;
- * the constituent linework will be extracted and used
- *
- * @param g a Geometry with linework to be polygonized
- */
-void
-Polygonizer::add(Geometry* g)
-{
-    g->apply_ro(&lineStringAdder);
-}
-
 /*
  * Add a geometry to the linework to be polygonized.
  * May be called multiple times.
@@ -128,6 +114,7 @@ Polygonizer::add(Geometry* g)
 void
 Polygonizer::add(const Geometry* g)
 {
+    util::ensureNotCurvedType(g);
     g->apply_ro(&lineStringAdder);
 }
 
diff --git a/src/triangulate/DelaunayTriangulationBuilder.cpp b/src/triangulate/DelaunayTriangulationBuilder.cpp
index 424de2a13..70bb217a9 100644
--- a/src/triangulate/DelaunayTriangulationBuilder.cpp
+++ b/src/triangulate/DelaunayTriangulationBuilder.cpp
@@ -77,6 +77,8 @@ DelaunayTriangulationBuilder::DelaunayTriangulationBuilder() :
 void
 DelaunayTriangulationBuilder::setSites(const Geometry& geom)
 {
+    util::ensureNotCurvedType(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 d4b09c8c5..29c05fee9 100644
--- a/src/triangulate/VoronoiDiagramBuilder.cpp
+++ b/src/triangulate/VoronoiDiagramBuilder.cpp
@@ -51,6 +51,7 @@ VoronoiDiagramBuilder::VoronoiDiagramBuilder() :
 void
 VoronoiDiagramBuilder::setSites(const geom::Geometry& geom)
 {
+    util::ensureNotCurvedType(geom);
     siteCoords = DelaunayTriangulationBuilder::extractUniqueCoordinates(geom);
     inputGeom = &geom;
 }
diff --git a/src/triangulate/polygon/ConstrainedDelaunayTriangulator.cpp b/src/triangulate/polygon/ConstrainedDelaunayTriangulator.cpp
index c52f85548..4788d3ba5 100644
--- a/src/triangulate/polygon/ConstrainedDelaunayTriangulator.cpp
+++ b/src/triangulate/polygon/ConstrainedDelaunayTriangulator.cpp
@@ -39,8 +39,10 @@ ConstrainedDelaunayTriangulator::triangulate(const Geometry* geom)
 
 /* private */
 std::unique_ptr<Geometry>
-ConstrainedDelaunayTriangulator::compute()
+ConstrainedDelaunayTriangulator::compute() const
 {
+    util::ensureNotCurvedType(inputGeom);
+
     // short circuit empty input case
     if(inputGeom->isEmpty()) {
         auto gf = inputGeom->getFactory();
diff --git a/tests/unit/capi/GEOSBoundaryTest.cpp b/tests/unit/capi/GEOSBoundaryTest.cpp
index 005ab388e..d24bd558f 100644
--- a/tests/unit/capi/GEOSBoundaryTest.cpp
+++ b/tests/unit/capi/GEOSBoundaryTest.cpp
@@ -30,6 +30,16 @@ void object::test<1>()
     ensure_equals(GEOSGetSRID(input_), GEOSGetSRID(result_));
 }
 
+template<>
+template<>
+void object::test<2>()
+{
+    input_ = fromWKT("CIRCULARSTRING (0 0, 1 1, 2 0)");
+    ensure(input_ != nullptr);
+
+    result_ = GEOSBoundary(input_);
+    ensure(result_ == nullptr);
+}
 
 } // namespace tut
 
diff --git a/tests/unit/capi/GEOSBufferTest.cpp b/tests/unit/capi/GEOSBufferTest.cpp
index e44d342e7..badf30684 100644
--- a/tests/unit/capi/GEOSBufferTest.cpp
+++ b/tests/unit/capi/GEOSBufferTest.cpp
@@ -460,4 +460,15 @@ void object::test<25>()
     ensure_geometry_equals(geom3_, geom2_, 0.001);
 }
 
+template<>
+template<>
+void object::test<26>()
+{
+    input_ = fromWKT("CIRCULARSTRING (0 0, 1 1, 2 0)");
+    ensure(input_ != nullptr);
+
+    result_ = GEOSBuffer(input_, 1, 8);
+    ensure(result_ == nullptr);
+}
+
 } // namespace tut
diff --git a/tests/unit/capi/GEOSBuildAreaTest.cpp b/tests/unit/capi/GEOSBuildAreaTest.cpp
index c7f23ac4b..012de41e4 100644
--- a/tests/unit/capi/GEOSBuildAreaTest.cpp
+++ b/tests/unit/capi/GEOSBuildAreaTest.cpp
@@ -38,4 +38,15 @@ void object::test<1>()
     ensure_geometry_equals(result_, expected_, 0);
 }
 
+template<>
+template<>
+void object::test<2>()
+{
+    input_ = fromWKT("MULTICURVE( CIRCULARSTRING (0 0, 1 1, 2 0), (2 0, 0 0) )");
+    ensure(input_ != nullptr);
+
+    result_ = GEOSBuildArea(input_);
+    ensure(result_ == nullptr);
+}
+
 } // namespace tut
diff --git a/tests/unit/capi/GEOSClipByRectTest.cpp b/tests/unit/capi/GEOSClipByRectTest.cpp
index 0fcdb530d..13fde6f73 100644
--- a/tests/unit/capi/GEOSClipByRectTest.cpp
+++ b/tests/unit/capi/GEOSClipByRectTest.cpp
@@ -271,6 +271,17 @@ void object::test<16>
     }
 }
 
+template<>
+template<>
+void object::test<17>()
+{
+    input_ = fromWKT("CIRCULARSTRING (0 0, 1 1, 2 0)");
+    ensure(input_ != nullptr);
+
+    result_ = GEOSClipByRect(input_, 0, 0, 1, 1);
+    ensure(result_ == nullptr);
+}
+
 
 } // namespace tut
 
diff --git a/tests/unit/capi/GEOSConcaveHullOfPolygonsTest.cpp b/tests/unit/capi/GEOSConcaveHullOfPolygonsTest.cpp
index 8de6baf54..57c6a0b9c 100644
--- a/tests/unit/capi/GEOSConcaveHullOfPolygonsTest.cpp
+++ b/tests/unit/capi/GEOSConcaveHullOfPolygonsTest.cpp
@@ -52,6 +52,17 @@ void object::test<2>()
     ensure_geometry_equals(geom1_, expected_);
 }
 
+template<>
+template<>
+void object::test<3>()
+{
+    input_ = fromWKT("CURVEPOLYGON (COMPOUNDCURVE( CIRCULARSTRING (0 0, 1 1, 2 0), (2 0, 0 0) ))");
+    ensure(input_ != nullptr);
+
+    result_ = GEOSConcaveHullOfPolygons(input_, 0.7, false, false);
+    ensure("curved geometry not supported", result_ == nullptr);
+}
+
 
 } // namespace tut
 
diff --git a/tests/unit/capi/GEOSConcaveHullTest.cpp b/tests/unit/capi/GEOSConcaveHullTest.cpp
index 690ce5425..243161d31 100644
--- a/tests/unit/capi/GEOSConcaveHullTest.cpp
+++ b/tests/unit/capi/GEOSConcaveHullTest.cpp
@@ -53,4 +53,15 @@ void object::test<2>()
     ensure_geometry_equals(result_, expected_);
 }
 
+template<>
+template<>
+void object::test<3>()
+{
+    input_ = fromWKT("CIRCULARSTRING (0 0, 1 1, 2 0)");
+    ensure(input_ != nullptr);
+
+    result_ = GEOSConcaveHull(input_, 0, 0);
+    ensure(result_ == nullptr);
+}
+
 } // namespace tut
diff --git a/tests/unit/capi/GEOSConstrainedDelaunayTriangulationTest.cpp b/tests/unit/capi/GEOSConstrainedDelaunayTriangulationTest.cpp
index 24687a6b8..2022b7f90 100644
--- a/tests/unit/capi/GEOSConstrainedDelaunayTriangulationTest.cpp
+++ b/tests/unit/capi/GEOSConstrainedDelaunayTriangulationTest.cpp
@@ -71,6 +71,17 @@ void object::test<3>
     ensure_geometry_equals(geom2_, geom3_);
 }
 
+// CurvePolygon
+template<>
+template<>
+void object::test<4>()
+{
+    input_ = fromWKT("CURVEPOLYGON (COMPOUNDCURVE (CIRCULARSTRING (0 0, 1 1, 2 0), (2 0, 0 0)))");
+    ensure(input_ != nullptr);
+
+    result_ = GEOSConstrainedDelaunayTriangulation(input_);
+    ensure("curved geometry not supported", result_ == nullptr);
+}
 
 
 } // namespace tut
diff --git a/tests/unit/capi/GEOSContainsTest.cpp b/tests/unit/capi/GEOSContainsTest.cpp
index 7dd017ff7..2e570424d 100644
--- a/tests/unit/capi/GEOSContainsTest.cpp
+++ b/tests/unit/capi/GEOSContainsTest.cpp
@@ -191,5 +191,19 @@ void object::test<5>
     }
 }
 
+template<>
+template<>
+void object::test<6>()
+{
+    geom1_ = fromWKT("CIRCULARSTRING (0 0, 1 1, 2 0)");
+    geom2_ = fromWKT("LINESTRING (1 0, 2 0)");
+
+    ensure(geom1_);
+    ensure(geom2_);
+
+    ensure_equals("curved geometry not supported", GEOSContains(geom1_, geom2_), 2);
+}
+
+
 } // namespace tut
 
diff --git a/tests/unit/capi/GEOSConvexHullTest.cpp b/tests/unit/capi/GEOSConvexHullTest.cpp
index 9b99c8cad..e7c20ac0b 100644
--- a/tests/unit/capi/GEOSConvexHullTest.cpp
+++ b/tests/unit/capi/GEOSConvexHullTest.cpp
@@ -42,5 +42,16 @@ void object::test<1>
     ensure_geometry_equals(result_, expected_);
 }
 
+template<>
+template<>
+void object::test<2>()
+{
+    input_ = fromWKT("CIRCULARSTRING (0 0, 1 1, 2 0)");
+    ensure(input_ != nullptr);
+
+    result_ = GEOSConvexHull(input_);
+    ensure(result_ == nullptr);
+}
+
 } // namespace tut
 
diff --git a/tests/unit/capi/GEOSCoverageIsValidTest.cpp b/tests/unit/capi/GEOSCoverageIsValidTest.cpp
index 79d54d88a..93868959d 100644
--- a/tests/unit/capi/GEOSCoverageIsValidTest.cpp
+++ b/tests/unit/capi/GEOSCoverageIsValidTest.cpp
@@ -88,6 +88,16 @@ template<> void object::test<2>
     ensure_geometry_equals(result_, expected_, 0.01);
 }
 
+template<>
+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_);
 
+    ensure_equals("curved geometry not supported", GEOSCoverageIsValid(input_, 0, nullptr), 2);
+}
 
 } // namespace tut
diff --git a/tests/unit/capi/GEOSCoverageSimplifyTest.cpp b/tests/unit/capi/GEOSCoverageSimplifyTest.cpp
index 830a58c47..812db7cc1 100644
--- a/tests/unit/capi/GEOSCoverageSimplifyTest.cpp
+++ b/tests/unit/capi/GEOSCoverageSimplifyTest.cpp
@@ -100,6 +100,18 @@ template<> void object::test<3>
     ensure_geometry_equals(result_, expected_, 0.1);
 }
 
+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_);
+
+    result_ = GEOSCoverageSimplifyVW(input_, 0.1, false);
+    ensure("curved geometry not supported", result_ == nullptr);
+}
 
 
 } // namespace tut
diff --git a/tests/unit/capi/GEOSCoverageUnionTest.cpp b/tests/unit/capi/GEOSCoverageUnionTest.cpp
index 6b8a71e7a..a9808c104 100644
--- a/tests/unit/capi/GEOSCoverageUnionTest.cpp
+++ b/tests/unit/capi/GEOSCoverageUnionTest.cpp
@@ -10,13 +10,15 @@
 #include <cstdlib>
 #include <cstring>
 
+#include "capi_test_utils.h"
+
 namespace tut {
 //
 // Test Group
 //
 
 // Common data used in test cases.
-struct test_capicoverageunion_data {
+struct test_capicoverageunion_data : public capitest::utility {
     static void
     notice(const char *fmt, ...) {
         std::fprintf(stdout, "NOTICE: ");
@@ -103,6 +105,18 @@ void object::test<2>
     GEOSGeom_destroy_r(m_context, result);
 }
 
+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_);
+
+    result_ = GEOSCoverageSimplifyVW(input_, 0.1, false);
+    ensure("curved geometry not supported", result_ == nullptr);
+}
 
 } // namespace tut
 
diff --git a/tests/unit/capi/GEOSCoveredByTest.cpp b/tests/unit/capi/GEOSCoveredByTest.cpp
index 6f9bfc947..6982fb496 100644
--- a/tests/unit/capi/GEOSCoveredByTest.cpp
+++ b/tests/unit/capi/GEOSCoveredByTest.cpp
@@ -32,5 +32,18 @@ void object::test<1>()
     ensure_equals(0, GEOSCoveredBy(geom3_, geom2_));
 }
 
+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)))");
+
+    ensure(geom1_);
+    ensure(geom2_);
+
+    ensure_equals("curved geometry not supported", GEOSCoveredBy(geom1_, geom2_), 2);
+}
+
 } // namespace tut
 
diff --git a/tests/unit/capi/GEOSCoversTest.cpp b/tests/unit/capi/GEOSCoversTest.cpp
index ee976743a..bdf5f3b08 100644
--- a/tests/unit/capi/GEOSCoversTest.cpp
+++ b/tests/unit/capi/GEOSCoversTest.cpp
@@ -32,5 +32,18 @@ void object::test<1>()
     ensure_equals(0, GEOSCovers(geom3_, geom2_));
 }
 
+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)");
+
+    ensure(geom1_);
+    ensure(geom2_);
+
+    ensure_equals("curved geometry not supported", GEOSCovers(geom1_, geom2_), 2);
+}
+
 } // namespace tut
 
diff --git a/tests/unit/capi/GEOSCrossesTest.cpp b/tests/unit/capi/GEOSCrossesTest.cpp
index ab19caa9f..12ba5f161 100644
--- a/tests/unit/capi/GEOSCrossesTest.cpp
+++ b/tests/unit/capi/GEOSCrossesTest.cpp
@@ -32,5 +32,19 @@ void object::test<1>()
     ensure_equals(0, GEOSCrosses(geom3_, geom2_));
 }
 
+template<>
+template<>
+void object::test<2>()
+{
+    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);
+}
+
 } // namespace tut
 
diff --git a/tests/unit/capi/GEOSDelaunayTriangulationTest.cpp b/tests/unit/capi/GEOSDelaunayTriangulationTest.cpp
index a5568e59e..0064d8073 100644
--- a/tests/unit/capi/GEOSDelaunayTriangulationTest.cpp
+++ b/tests/unit/capi/GEOSDelaunayTriangulationTest.cpp
@@ -128,5 +128,16 @@ void object::test<6>
                       "MULTILINESTRING ((10 0, 10 10), (0 0, 10 10), (0 0, 10 0))");
 }
 
+template<>
+template<>
+void object::test<7>()
+{
+    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);
+}
+
 } // namespace tut
 
diff --git a/tests/unit/capi/GEOSDensifyTest.cpp b/tests/unit/capi/GEOSDensifyTest.cpp
index cd11e1cea..97ede1605 100644
--- a/tests/unit/capi/GEOSDensifyTest.cpp
+++ b/tests/unit/capi/GEOSDensifyTest.cpp
@@ -159,4 +159,14 @@ void object::test<9>()
     ensure("result expected to be NULL", result_ == nullptr);
 }
 
+template<>
+template<>
+void object::test<10>()
+{
+    input_ = fromWKT("CIRCULARSTRING (0 0, 1 1, 2 0)");
+    ensure(input_);
+
+    result_ = GEOSDensify(input_, 0.1);
+    ensure("curved geometries not supported", result_ == nullptr);
+}
 } // namespace tut
diff --git a/tests/unit/capi/GEOSDifferenceTest.cpp b/tests/unit/capi/GEOSDifferenceTest.cpp
index 5cdf79604..d739b0936 100644
--- a/tests/unit/capi/GEOSDifferenceTest.cpp
+++ b/tests/unit/capi/GEOSDifferenceTest.cpp
@@ -37,8 +37,7 @@ void object::test<1>()
 */
 template<>
 template<>
-void object::test<2>()
-{
+void object::test<2>() {
     GEOSGeometry* a = GEOSGeomFromWKT("GEOMETRYCOLLECTION (POINT (51 -1), LINESTRING (52 -1, 49 2))");
     GEOSGeometry* b = GEOSGeomFromWKT("POINT (2 3)");
 
@@ -60,5 +59,19 @@ void object::test<2>()
     GEOSGeom_destroy(ba);
 }
 
+template<>
+template<>
+void object::test<3>()
+{
+    geom1_ = fromWKT("CIRCULARSTRING (0 0, 1 1, 2 0)");
+    geom2_ = fromWKT("LINESTRING (1 0, 2 1)");
+
+    ensure(geom1_);
+    ensure(geom2_);
+
+    result_ = GEOSDifference(geom1_, geom2_);
+    ensure("curved geometry not supported", result_ == nullptr);
+}
+
 } // namespace tut
 
diff --git a/tests/unit/capi/GEOSDisjointSubsetUnionTest.cpp b/tests/unit/capi/GEOSDisjointSubsetUnionTest.cpp
index 456b39ee9..76e2a3992 100644
--- a/tests/unit/capi/GEOSDisjointSubsetUnionTest.cpp
+++ b/tests/unit/capi/GEOSDisjointSubsetUnionTest.cpp
@@ -46,6 +46,15 @@ void object::test<2>()
     ensure_geometry_equals(result_, expected_);
 }
 
+template <>
+template <>
+void object::test<3>() {
+    input_ = fromWKT("MULTICURVE ((0 0, 1 1), CIRCULARSTRING (1 1, 2 0, 3 1), (5 5, 8 8))");
+    ensure(input_);
+
+    result_ = GEOSDisjointSubsetUnion(input_);
+    ensure("curved geometry not supported", result_ == nullptr);
+}
 
 } // namespace tut
 
diff --git a/tests/unit/capi/GEOSDisjointTest.cpp b/tests/unit/capi/GEOSDisjointTest.cpp
index cb8fde2a2..f17faca3a 100644
--- a/tests/unit/capi/GEOSDisjointTest.cpp
+++ b/tests/unit/capi/GEOSDisjointTest.cpp
@@ -28,5 +28,19 @@ void object::test<1>()
     ensure_equals(0, GEOSDisjoint(geom1_, geom3_));
 }
 
+template<>
+template<>
+void object::test<2>()
+{
+    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", GEOSDisjoint(geom1_, geom2_), 2);
+    ensure_equals("curved geometry not supported", GEOSDisjoint(geom2_, geom1_), 2);
+}
+
 } // namespace tut
 
diff --git a/tests/unit/capi/GEOSDistanceTest.cpp b/tests/unit/capi/GEOSDistanceTest.cpp
index 5bf03c90b..0416532ef 100644
--- a/tests/unit/capi/GEOSDistanceTest.cpp
+++ b/tests/unit/capi/GEOSDistanceTest.cpp
@@ -157,5 +157,20 @@ void object::test<5>
     ensure_equals(raised & FE_OVERFLOW, 0);
 }
 
+template<>
+template<>
+void object::test<6>()
+{
+    geom1_ = fromWKT("CIRCULARSTRING (0 0, 1 1, 2 0)");
+    geom2_ = fromWKT("LINESTRING (1 1.0001, 2 1)");
+
+    ensure(geom1_);
+    ensure(geom2_);
+
+    double dist;
+    int ret = GEOSDistance(geom1_, geom2_, &dist);
+    ensure_equals("curved geometry not supported", ret, 0);
+}
+
 } // namespace tut
 
diff --git a/tests/unit/capi/GEOSDistanceWithinTest.cpp b/tests/unit/capi/GEOSDistanceWithinTest.cpp
index a76aa05e9..567e43192 100644
--- a/tests/unit/capi/GEOSDistanceWithinTest.cpp
+++ b/tests/unit/capi/GEOSDistanceWithinTest.cpp
@@ -415,6 +415,19 @@ void object::test<31>() {
     );
 }
 
+template<>
+template<>
+void object::test<32>()
+{
+    geom1_ = fromWKT("CIRCULARSTRING (0 0, 1 1, 2 0)");
+    geom2_ = fromWKT("LINESTRING (1 1.0001, 2 1)");
+
+    ensure(geom1_);
+    ensure(geom2_);
+
+    char ret = GEOSDistanceWithin(geom1_, geom2_, 0.1);
+    ensure_equals("curved geometry not supported", ret, 2);
+}
 
 
 
diff --git a/tests/unit/capi/GEOSEqualsIdenticalTest.cpp b/tests/unit/capi/GEOSEqualsIdenticalTest.cpp
index 4b0da5299..ea2b2a799 100644
--- a/tests/unit/capi/GEOSEqualsIdenticalTest.cpp
+++ b/tests/unit/capi/GEOSEqualsIdenticalTest.cpp
@@ -212,5 +212,16 @@ void object::test<16>
     ensure_equals(GEOSEqualsIdentical(geom1_, geom2_), 1);
 }
 
+template<>
+template<>
+void object::test<17>
+()
+{
+    geom1_ = GEOSGeomFromWKT("CIRCULARSTRING (0 0, 1 1, 2 0)");
+    geom2_ = GEOSGeomFromWKT("CIRCULARSTRING (0 0, 1 1, 2 0)");
+
+    ensure_equals(GEOSEqualsIdentical(geom1_, geom2_), 1);
+}
+
 } // namespace tut
 
diff --git a/tests/unit/capi/GEOSEqualsTest.cpp b/tests/unit/capi/GEOSEqualsTest.cpp
index 793207f49..0164a3ced 100644
--- a/tests/unit/capi/GEOSEqualsTest.cpp
+++ b/tests/unit/capi/GEOSEqualsTest.cpp
@@ -134,5 +134,19 @@ void object::test<6>
 #endif
 
 
+template<>
+template<>
+void object::test<7>()
+{
+    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);
+}
+
 } // namespace tut
 
diff --git a/tests/unit/capi/GEOSFrechetDistanceTest.cpp b/tests/unit/capi/GEOSFrechetDistanceTest.cpp
index 93c0f657e..ad6215a28 100644
--- a/tests/unit/capi/GEOSFrechetDistanceTest.cpp
+++ b/tests/unit/capi/GEOSFrechetDistanceTest.cpp
@@ -92,4 +92,19 @@ void object::test<4>
     ensure(dist >= 0); // no crash
 }
 
+template<>
+template<>
+void object::test<5>()
+{
+    geom1_ = fromWKT("CIRCULARSTRING (0 0, 1 1, 2 0)");
+    geom2_ = fromWKT("LINESTRING (1 2, 2 2)");
+
+    ensure(geom1_);
+    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);
+}
+
 } // namespace tut
diff --git a/tests/unit/capi/GEOSGeomGeoJSONWriteTest.cpp b/tests/unit/capi/GEOSGeomGeoJSONWriteTest.cpp
index 939ec6e5f..23b2d68d1 100644
--- a/tests/unit/capi/GEOSGeomGeoJSONWriteTest.cpp
+++ b/tests/unit/capi/GEOSGeomGeoJSONWriteTest.cpp
@@ -100,4 +100,17 @@ void object::test<3>
         "}");
     test_geojson(wkt, expected, 4);
 }
+
+template<>
+template<>
+void object::test<4>
+()
+{
+    input_ = fromWKT("CIRCULARSTRING (0 0, 1 1, 2 0)");
+    ensure(input_);
+
+    char* geojson = GEOSGeoJSONWriter_writeGeometry(writer_, input_, 0);
+    ensure("curved geometry not supported", geojson == nullptr);
+}
+
 }
diff --git a/tests/unit/capi/GEOSGeomGetNumPointsTest.cpp b/tests/unit/capi/GEOSGeomGetNumPointsTest.cpp
index 7c9db7ae2..80b7d2456 100644
--- a/tests/unit/capi/GEOSGeomGetNumPointsTest.cpp
+++ b/tests/unit/capi/GEOSGeomGetNumPointsTest.cpp
@@ -40,5 +40,13 @@ void object::test<3>()
     ensure_equals(GEOSGeomGetNumPoints(input_), -1);
 }
 
+template<>
+template<>
+void object::test<4>()
+{
+    input_ = fromWKT("CIRCULARSTRING (0 0, 1 1, 2 0)");
+    ensure_equals(GEOSGeomGetNumPoints(input_), 3);
+}
+
 } // namespace tut
 
diff --git a/tests/unit/capi/GEOSGeomToHEX_bufTest.cpp b/tests/unit/capi/GEOSGeomToHEX_bufTest.cpp
index fbd43b0a0..f6d6c4e7c 100644
--- a/tests/unit/capi/GEOSGeomToHEX_bufTest.cpp
+++ b/tests/unit/capi/GEOSGeomToHEX_bufTest.cpp
@@ -78,5 +78,7 @@ void object::test<3>()
     GEOS_finish_r(handle);
 }
 
+
+
 } // namespace tut
 
diff --git a/tests/unit/capi/GEOSGeomTypeIdTest.cpp b/tests/unit/capi/GEOSGeomTypeIdTest.cpp
index 65eed116a..326796a25 100644
--- a/tests/unit/capi/GEOSGeomTypeIdTest.cpp
+++ b/tests/unit/capi/GEOSGeomTypeIdTest.cpp
@@ -24,9 +24,12 @@ void object::test<1>()
     ensure(nullptr != geom1_);
     geom2_ = fromWKT("LINESTRING (1 2, 3 4)");
     ensure(nullptr != geom2_);
+    geom3_ = fromWKT("CIRCULARSTRING (0 0, 1 1, 2 0)");
+    ensure(nullptr != geom3_);
 
-    ensure_equals(0, GEOSGeomTypeId(geom1_));
-    ensure_equals(1, GEOSGeomTypeId(geom2_));
+    ensure_equals(GEOS_POINT, GEOSGeomTypeId(geom1_));
+    ensure_equals(GEOS_LINESTRING, GEOSGeomTypeId(geom2_));
+    ensure_equals(GEOS_CIRCULARSTRING, GEOSGeomTypeId(geom3_));
 }
 
 } // namespace tut
diff --git a/tests/unit/capi/GEOSGeomTypeTest.cpp b/tests/unit/capi/GEOSGeomTypeTest.cpp
index 14ba449f6..3f88c9431 100644
--- a/tests/unit/capi/GEOSGeomTypeTest.cpp
+++ b/tests/unit/capi/GEOSGeomTypeTest.cpp
@@ -29,6 +29,11 @@ void object::test<1>()
     char* type2 = GEOSGeomType(geom2_);
     ensure_equals(std::string(type2), "LineString");
     GEOSFree(type2);
+
+    geom3_ = fromWKT("CIRCULARSTRING (0 0, 1 1, 2 2)");
+    char* type3 = GEOSGeomType(geom3_);
+    ensure_equals(std::string(type3), "CircularString");
+    GEOSFree(type3);
 }
 
 } // namespace tut
diff --git a/tests/unit/capi/GEOSGeom_cloneTest.cpp b/tests/unit/capi/GEOSGeom_cloneTest.cpp
index 9948d5a0f..3466a1b95 100644
--- a/tests/unit/capi/GEOSGeom_cloneTest.cpp
+++ b/tests/unit/capi/GEOSGeom_cloneTest.cpp
@@ -34,6 +34,16 @@ void object::test<1>
     ensure(GEOSGeom_getUserData(geom2_) == nullptr); // userData not transferred
 }
 
+template<>
+template<>
+void object::test<2>
+()
+{
+    input_ = GEOSGeomFromWKT("CIRCULARSTRING (0 0, 1 1, 2 0)");
+    result_ = GEOSGeom_clone(input_);
+
+    ensure_equals(toWKT(result_), "CIRCULARSTRING (0 0, 1 1, 2 0)");
+}
 
 } // namespace tut
 
diff --git a/tests/unit/capi/GEOSGeom_extractUniquePointsTest.cpp b/tests/unit/capi/GEOSGeom_extractUniquePointsTest.cpp
index b2b531b36..3d5c98614 100644
--- a/tests/unit/capi/GEOSGeom_extractUniquePointsTest.cpp
+++ b/tests/unit/capi/GEOSGeom_extractUniquePointsTest.cpp
@@ -65,6 +65,17 @@ void object::test<3>
     ensure_geometry_equals(geom3_, geom2_);
 }
 
+template <>
+template <>
+void object::test<4>() {
+    input_ = GEOSGeomFromWKT("CIRCULARSTRING (0 0, 1 1, 2 0)");
+    ensure(input_);
+
+    result_ = GEOSGeom_extractUniquePoints(input_);
+
+    ensure_geometry_equals(result_, "MULTIPOINT ((0 0), (1 1), (2 0))");
+}
+
 
 } // namespace tut
 
diff --git a/tests/unit/capi/GEOSGeom_getCoordSeqTest.cpp b/tests/unit/capi/GEOSGeom_getCoordSeqTest.cpp
index bd8459788..e976d8ef3 100644
--- a/tests/unit/capi/GEOSGeom_getCoordSeqTest.cpp
+++ b/tests/unit/capi/GEOSGeom_getCoordSeqTest.cpp
@@ -57,5 +57,19 @@ void object::test<3>()
     ensure_equals(y, 8);
 }
 
+template<>
+template<>
+void object::test<4>()
+{
+    input_ = fromWKT("CIRCULARSTRING (0 0, 1 1, 2 0)");
+    const GEOSCoordSequence* seq = GEOSGeom_getCoordSeq(input_);
+
+    ensure(seq);
+
+    unsigned int size;
+    ensure(GEOSCoordSeq_getSize(seq, &size));
+    ensure_equals(size, 3u);
+}
+
 } // namespace tut
 
diff --git a/tests/unit/capi/GEOSGeom_getCoordinateDimensionTest.cpp b/tests/unit/capi/GEOSGeom_getCoordinateDimensionTest.cpp
index 8d0b0ce6f..c3f41676b 100644
--- a/tests/unit/capi/GEOSGeom_getCoordinateDimensionTest.cpp
+++ b/tests/unit/capi/GEOSGeom_getCoordinateDimensionTest.cpp
@@ -21,14 +21,35 @@ template<>
 void object::test<1>
 ()
 {
-    geom1_ = fromWKT("POLYGON ((0 0, 1 0, 1 1, 0 0))");
-    ensure_equals(GEOSGeom_getCoordinateDimension(geom1_), 2);
+    input_ = fromWKT("POLYGON ((0 0, 1 0, 1 1, 0 0))");
+    ensure_equals(GEOSGeom_getCoordinateDimension(input_), 2);
+}
 
-    geom2_ = fromWKT("POINT (4 2 7)");
-    ensure_equals(GEOSGeom_getCoordinateDimension(geom2_), 3);
+template<>
+template<>
+void object::test<2>
+()
+{
+    input_ = fromWKT("POINT (4 2 7)");
+    ensure_equals(GEOSGeom_getCoordinateDimension(input_), 3);
+}
 
-    geom3_ = fromWKT("LINESTRING (4 2 7 1, 8 2 9 5)");
-    ensure_equals(GEOSGeom_getCoordinateDimension(geom3_), 4);
+template<>
+template<>
+void object::test<3>
+()
+{
+    input_ = fromWKT("LINESTRING (4 2 7 1, 8 2 9 5)");
+    ensure_equals(GEOSGeom_getCoordinateDimension(input_), 4);
+}
+
+template<>
+template<>
+void object::test<4>
+()
+{
+    input_ = fromWKT("CIRCULARSTRING Z (0 0 0, 1 1 1, 2 0 2)");
+    ensure_equals(GEOSGeom_getCoordinateDimension(input_), 3);
 }
 
 } // namespace tut
diff --git a/tests/unit/capi/GEOSGeom_getDimensionsTest.cpp b/tests/unit/capi/GEOSGeom_getDimensionsTest.cpp
index c90e3e0ae..5050f5d69 100644
--- a/tests/unit/capi/GEOSGeom_getDimensionsTest.cpp
+++ b/tests/unit/capi/GEOSGeom_getDimensionsTest.cpp
@@ -31,5 +31,13 @@ void object::test<1>
     ensure_equals(GEOSGeom_getDimensions(geom2_), 0);
 }
 
+template<>
+template<>
+void object::test<2>()
+{
+    input_ = fromWKT("CIRCULARSTRING (0 0, 1 1, 2 0)");
+    ensure_equals(GEOSGeom_getDimensions(input_), 1);
+}
+
 } // namespace tut
 
diff --git a/tests/unit/capi/GEOSGeom_transformXYTest.cpp b/tests/unit/capi/GEOSGeom_transformXYTest.cpp
index f220a47f2..b2112ea36 100644
--- a/tests/unit/capi/GEOSGeom_transformXYTest.cpp
+++ b/tests/unit/capi/GEOSGeom_transformXYTest.cpp
@@ -294,4 +294,15 @@ void object::test<13>() {
     GEOSGeom_destroy(out);
 }
 
+template <>
+template <>
+void object::test<14>() {
+    input_ = GEOSGeomFromWKT("CIRCULARSTRING (0 0, 1 1, 2 0)");
+    ensure(input_);
+
+    result_ = GEOSGeom_transformXY(input_, SCALE_2_3, nullptr);
+
+    ensure_equals(toWKT(result_), "CIRCULARSTRING (0 0, 2 3, 4 0)");
+}
+
 }  // namespace tut
diff --git a/tests/unit/capi/GEOSGetCentroidTest.cpp b/tests/unit/capi/GEOSGetCentroidTest.cpp
index 96b6334be..f99c78f12 100644
--- a/tests/unit/capi/GEOSGetCentroidTest.cpp
+++ b/tests/unit/capi/GEOSGetCentroidTest.cpp
@@ -139,5 +139,16 @@ void object::test<5>
 
 }
 
+template<>
+template<>
+void object::test<6>()
+{
+    input_ = fromWKT("CURVEPOLYGON (COMPOUNDCURVE (CIRCULARSTRING (0 0, 1 1, 2 0), (2 0, 0 0)))");
+    ensure(input_ != nullptr);
+
+    result_ = GEOSGetCentroid(input_);
+    ensure("curved geometry not supported", result_ == nullptr);
+}
+
 } // namespace tut
 
diff --git a/tests/unit/capi/GEOSGetExteriorRingTest.cpp b/tests/unit/capi/GEOSGetExteriorRingTest.cpp
index 0905b2e55..c8012f90a 100644
--- a/tests/unit/capi/GEOSGetExteriorRingTest.cpp
+++ b/tests/unit/capi/GEOSGetExteriorRingTest.cpp
@@ -41,4 +41,17 @@ namespace tut
         ensure(GEOSGetExteriorRing(geom1_) == nullptr);
     }
 
+    template<>
+    template<>
+    void object::test<3>()
+    {
+        input_ = fromWKT("CURVEPOLYGON (COMPOUNDCURVE (CIRCULARSTRING (0 0, 1 1, 2 0), (2 0, 0 0)))");
+        ensure(input_ != nullptr);
+
+        const GEOSGeometry* extring = GEOSGetExteriorRing(input_);
+        ensure(extring);
+
+        ensure_equals(toWKT(extring), "COMPOUNDCURVE (CIRCULARSTRING (0 0, 1 1, 2 0), (2 0, 0 0))");
+    }
+
 } // namespace tut
diff --git a/tests/unit/capi/GEOSGetGeometryNTest.cpp b/tests/unit/capi/GEOSGetGeometryNTest.cpp
index 92665cbc1..bb8ac8d35 100644
--- a/tests/unit/capi/GEOSGetGeometryNTest.cpp
+++ b/tests/unit/capi/GEOSGetGeometryNTest.cpp
@@ -23,16 +23,27 @@ void object::test<1>()
     geom1_ = fromWKT("MULTIPOINT ((1 1), (2 2), (3 3))");
     ensure(nullptr != geom1_);
 
-    GEOSGeometry* result = const_cast<GEOSGeometry*>(GEOSGetGeometryN(geom1_, 0));
+    const GEOSGeometry* result = GEOSGetGeometryN(geom1_, 0);
     ensure(nullptr != result);
     ensure_equals("POINT (1 1)", toWKT(result));
     
-    result = const_cast<GEOSGeometry*>(GEOSGetGeometryN(geom1_, 2));
+    result = GEOSGetGeometryN(geom1_, 2);
     ensure(nullptr != result);
     ensure_equals("POINT (3 3)", toWKT(result));
 
     ensure(GEOSGetGeometryN(geom1_, -1) == nullptr);
 }
 
+template<>
+template<>
+void object::test<2>()
+{
+    input_ = fromWKT("MULTICURVE ((0 0, 1 1), CIRCULARSTRING (1 1, 2 0, 3 1))");
+    ensure(input_);
+
+    ensure_equals(toWKT(GEOSGetGeometryN(input_, 0)), "LINESTRING (0 0, 1 1)");
+    ensure_equals(toWKT(GEOSGetGeometryN(input_, 1)), "CIRCULARSTRING (1 1, 2 0, 3 1)");
+}
+
 } // namespace tut
 
diff --git a/tests/unit/capi/GEOSGetInteriorRingNTest.cpp b/tests/unit/capi/GEOSGetInteriorRingNTest.cpp
index d54664d90..e9b50d60e 100644
--- a/tests/unit/capi/GEOSGetInteriorRingNTest.cpp
+++ b/tests/unit/capi/GEOSGetInteriorRingNTest.cpp
@@ -42,5 +42,17 @@ namespace tut
         ensure(GEOSGetInteriorRingN(geom1_, 0) == nullptr);
     }
 
+    template<>
+    template<>
+    void object::test<3>()
+    {
+        input_ = fromWKT("CURVEPOLYGON (COMPOUNDCURVE (CIRCULARSTRING (0 0, 10 10, 20 0), (20 0, 0 0)), (8 8, 9 9, 9 8, 8 8))");
+        ensure(input_ != nullptr);
+
+        const GEOSGeometry* intring = GEOSGetInteriorRingN(input_, 0);
+        ensure(intring);
+
+        ensure_equals(toWKT(intring), "LINESTRING (8 8, 9 9, 9 8, 8 8)");
+    }
 
 } // namespace tut
diff --git a/tests/unit/capi/GEOSGetNumInteriorRingsTest.cpp b/tests/unit/capi/GEOSGetNumInteriorRingsTest.cpp
index 968868ea4..70e647990 100644
--- a/tests/unit/capi/GEOSGetNumInteriorRingsTest.cpp
+++ b/tests/unit/capi/GEOSGetNumInteriorRingsTest.cpp
@@ -38,5 +38,15 @@ void object::test<2>()
     GEOSGeom_destroy(input);
 }
 
+template<>
+template<>
+void object::test<3>()
+{
+    input_ = fromWKT("CURVEPOLYGON (COMPOUNDCURVE (CIRCULARSTRING (0 0, 10 10, 20 0), (20 0, 0 0)), (8 8, 9 9, 9 8, 8 8))");
+    ensure(input_ != nullptr);
+
+    ensure_equals(GEOSGetNumInteriorRings(input_), 1);
+}
+
 } // namespace tut
 
diff --git a/tests/unit/capi/GEOSHasZMTest.cpp b/tests/unit/capi/GEOSHasZMTest.cpp
index 346ca97d8..13ea525b9 100644
--- a/tests/unit/capi/GEOSHasZMTest.cpp
+++ b/tests/unit/capi/GEOSHasZMTest.cpp
@@ -106,6 +106,15 @@ void object::test<9>()
     ensure_equals(GEOSHasM(input_), 1);
 }
 
+template<>
+template<>
+void object::test<10>()
+{
+    input_ = fromWKT("CIRCULARSTRING M (0 0 0, 1 1 1, 2 0 2)");
+
+    ensure_equals(GEOSHasZ(input_), 0);
+    ensure_equals(GEOSHasM(input_), 1);
+}
 
 } // namespace tut
 
diff --git a/tests/unit/capi/GEOSHausdorffDistanceTest.cpp b/tests/unit/capi/GEOSHausdorffDistanceTest.cpp
index 3cddf2424..ead81cd93 100644
--- a/tests/unit/capi/GEOSHausdorffDistanceTest.cpp
+++ b/tests/unit/capi/GEOSHausdorffDistanceTest.cpp
@@ -63,4 +63,19 @@ void object::test<2>
     ensure_distance(dist, 70., 1e-12);
 }
 
+template<>
+template<>
+void object::test<3>()
+{
+    geom1_ = fromWKT("CIRCULARSTRING (0 0, 1 1, 2 0)");
+    geom2_ = fromWKT("LINESTRING (1 2, 2 2)");
+
+    ensure(geom1_);
+    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);
+}
+
 } // namespace tut
diff --git a/tests/unit/capi/GEOSIntersectionTest.cpp b/tests/unit/capi/GEOSIntersectionTest.cpp
index 94d209e26..2377c609f 100644
--- a/tests/unit/capi/GEOSIntersectionTest.cpp
+++ b/tests/unit/capi/GEOSIntersectionTest.cpp
@@ -143,6 +143,19 @@ void object::test<7>
     ensure(!std::fetestexcept(FE_INVALID));
 }
 
+template<>
+template<>
+void object::test<8>()
+{
+    geom1_ = fromWKT("CIRCULARSTRING (0 0, 1 1, 2 0)");
+    geom2_ = fromWKT("LINESTRING (1 0, 2 1)");
+
+    ensure(geom1_);
+    ensure(geom2_);
+
+    result_ = GEOSIntersection(geom1_, geom2_);
+    ensure("curved geometry not supported", result_ == nullptr);
+}
 
 } // namespace tut
 
diff --git a/tests/unit/capi/GEOSIntersectsTest.cpp b/tests/unit/capi/GEOSIntersectsTest.cpp
index bba253445..395d2698c 100644
--- a/tests/unit/capi/GEOSIntersectsTest.cpp
+++ b/tests/unit/capi/GEOSIntersectsTest.cpp
@@ -220,5 +220,19 @@ void object::test<10>
     ensure_equals(r2, 1);
 }
 
+template<>
+template<>
+void object::test<11>()
+{
+    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", GEOSIntersects(geom1_, geom2_), 2);
+    ensure_equals("curved geometry not supported", GEOSIntersects(geom2_, geom1_), 2);
+}
+
 } // namespace tut
 
diff --git a/tests/unit/capi/GEOSLargestEmptyCircleTest.cpp b/tests/unit/capi/GEOSLargestEmptyCircleTest.cpp
index aff84e2ec..6b0e69a86 100644
--- a/tests/unit/capi/GEOSLargestEmptyCircleTest.cpp
+++ b/tests/unit/capi/GEOSLargestEmptyCircleTest.cpp
@@ -65,5 +65,32 @@ void object::test<2>
     ensure_geometry_equals_exact(result_, expected_, 0.0001);
 }
 
+template<>
+template<>
+void object::test<3>()
+{
+    input_ = fromWKT("MULTICURVE (CIRCULARSTRING (0 0, 1 1, 2 0), (0 3, 2 3))");
+    ensure(input_);
+
+    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)))");
+    ensure(input_);
+    ensure(geom2_);
+
+    result_ = GEOSLargestEmptyCircle(input_, geom2_, 0.001);
+
+    ensure("curved geometries not supported", result_ == nullptr);
+}
+
+
 } // namespace tut
 
diff --git a/tests/unit/capi/GEOSLengthTest.cpp b/tests/unit/capi/GEOSLengthTest.cpp
index fa22e2a7a..66c2872d1 100644
--- a/tests/unit/capi/GEOSLengthTest.cpp
+++ b/tests/unit/capi/GEOSLengthTest.cpp
@@ -58,5 +58,17 @@ void object::test<3>()
     ensure_equals(length, 0);
 }
 
+template<>
+template<>
+void object::test<4>()
+{
+    input_ = fromWKT("CIRCULARSTRING (0 0, 1 1, 2 0)");
+    ensure(input_ != nullptr);
+
+    double length = -1;
+    int ret = GEOSLength(input_, &length);
+    ensure_equals("error raised on curved geometry", ret, 0);
+}
+
 } // namespace tut
 
diff --git a/tests/unit/capi/GEOSLineMergeDirectedTest.cpp b/tests/unit/capi/GEOSLineMergeDirectedTest.cpp
index 39a172ad1..bd5537d08 100644
--- a/tests/unit/capi/GEOSLineMergeDirectedTest.cpp
+++ b/tests/unit/capi/GEOSLineMergeDirectedTest.cpp
@@ -60,5 +60,18 @@ void object::test<2>
     GEOSGeom_destroy(expected);
 }
 
+
+template<>
+template<>
+void object::test<3>()
+{
+    input_ = fromWKT("MULTICURVE (CIRCULARSTRING (0 0, 1 1, 2 0), (2 0, 3 0))");
+    ensure(input_);
+
+    result_ = GEOSLineMergeDirected(input_);
+
+    ensure("curved geometries not supported", result_ == nullptr);
+}
+
 } // namesplace tut
 
diff --git a/tests/unit/capi/GEOSLineMergeTest.cpp b/tests/unit/capi/GEOSLineMergeTest.cpp
index e744ed513..d8c40c4e0 100644
--- a/tests/unit/capi/GEOSLineMergeTest.cpp
+++ b/tests/unit/capi/GEOSLineMergeTest.cpp
@@ -44,5 +44,17 @@ void object::test<1>
     GEOSGeom_destroy(expected);
 }
 
+template<>
+template<>
+void object::test<2>()
+{
+    input_ = fromWKT("MULTICURVE (CIRCULARSTRING (0 0, 1 1, 2 0), (2 0, 3 0))");
+    ensure(input_);
+
+    result_ = GEOSLineMerge(input_);
+
+    ensure("curved geometries not supported", result_ == nullptr);
+}
+
 } // namespace tut
 
diff --git a/tests/unit/capi/GEOSLineSubstringTest.cpp b/tests/unit/capi/GEOSLineSubstringTest.cpp
index c75930792..24078c085 100644
--- a/tests/unit/capi/GEOSLineSubstringTest.cpp
+++ b/tests/unit/capi/GEOSLineSubstringTest.cpp
@@ -119,6 +119,17 @@ void object::test<6>
     ensure_geometry_equals(result_, expected_);
 }
 
+template<>
+template<>
+void object::test<7>()
+{
+    input_ = fromWKT("CIRCULARSTRING (0 0, 1 1, 2 0)");
+    ensure(input_);
+
+    result_ = GEOSLineSubstring(input_, 0.5, 0);
+
+    ensure("curved geometries not supported", result_ == nullptr);
+}
 
 } // namespace tut
 
diff --git a/tests/unit/capi/GEOSMakeValidTest.cpp b/tests/unit/capi/GEOSMakeValidTest.cpp
index c28d14606..1c6071e94 100644
--- a/tests/unit/capi/GEOSMakeValidTest.cpp
+++ b/tests/unit/capi/GEOSMakeValidTest.cpp
@@ -94,5 +94,16 @@ void object::test<4>
     ensure(GEOSEqualsExact(geom2_, expected_, 0.01));
 }
 
+template<>
+template<>
+void object::test<5>()
+{
+    // 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_);
+
+    result_ = GEOSMakeValid(input_);
+    ensure("curved geometry not supported", result_ == nullptr);
+}
 
 } // namespace tut
diff --git a/tests/unit/capi/GEOSMaximumInscribedCircleTest.cpp b/tests/unit/capi/GEOSMaximumInscribedCircleTest.cpp
index 60ef3f10b..1ca8a7936 100644
--- a/tests/unit/capi/GEOSMaximumInscribedCircleTest.cpp
+++ b/tests/unit/capi/GEOSMaximumInscribedCircleTest.cpp
@@ -75,7 +75,16 @@ void object::test<2>
     // no crash
 }
 
+template<>
+template<>
+void object::test<3>()
+{
+    input_ = fromWKT("CURVEPOLYGON (COMPOUNDCURVE (CIRCULARSTRING (0 0, 10 10, 20 0), (20 0, 0 0)))");
+    ensure(input_);
 
+    result_ = GEOSMaximumInscribedCircle(input_, 1);
+    ensure("curved geometry not supported", result_ == nullptr);
+}
 
 } // namespace tut
 
diff --git a/tests/unit/capi/GEOSMinimumBoundingCircleTest.cpp b/tests/unit/capi/GEOSMinimumBoundingCircleTest.cpp
index 3bfd6326a..5b7c1dff4 100644
--- a/tests/unit/capi/GEOSMinimumBoundingCircleTest.cpp
+++ b/tests/unit/capi/GEOSMinimumBoundingCircleTest.cpp
@@ -116,5 +116,19 @@ void object::test<4>
     ensure(fabs(radius) - 5.0 < 0.001);
 }
 
+template<>
+template<>
+void object::test<5>
+()
+{
+    input_ = fromWKT("CIRCULARSTRING (0 0, 1 1, 2 0)");
+    ensure(input_);
+
+    GEOSGeometry* center;
+    double radius;
+    result_ = GEOSMinimumBoundingCircle(input_, &radius, &center);
+    ensure("curved geometry not supported", result_ == nullptr);
+}
+
 } // namespace tut
 
diff --git a/tests/unit/capi/GEOSMinimumClearanceTest.cpp b/tests/unit/capi/GEOSMinimumClearanceTest.cpp
index f683145da..0e7f65e07 100644
--- a/tests/unit/capi/GEOSMinimumClearanceTest.cpp
+++ b/tests/unit/capi/GEOSMinimumClearanceTest.cpp
@@ -120,4 +120,5 @@ void object::test<5>
                   geos::DoubleInfinity);
 }
 
+
 } // namespace tut
diff --git a/tests/unit/capi/GEOSMinimumRotatedRectangleTest.cpp b/tests/unit/capi/GEOSMinimumRotatedRectangleTest.cpp
index a71462d86..6782d20c2 100644
--- a/tests/unit/capi/GEOSMinimumRotatedRectangleTest.cpp
+++ b/tests/unit/capi/GEOSMinimumRotatedRectangleTest.cpp
@@ -122,5 +122,16 @@ void object::test<7>
         "POLYGON ((1 2, 3 8, 9 6, 7 0, 1 2))");
 }
 
+template<>
+template<>
+void object::test<8>
+()
+{
+    input_ = fromWKT("CIRCULARSTRING (0 0, 1 1, 2 0)");
+    ensure(input_);
+
+    result_ = GEOSMinimumRotatedRectangle(input_);
+    ensure("curved geometry not supported", result_ == nullptr);
+}
 
 } // namespace tut
diff --git a/tests/unit/capi/GEOSMinimumWidthTest.cpp b/tests/unit/capi/GEOSMinimumWidthTest.cpp
index bad070c10..57f935da8 100644
--- a/tests/unit/capi/GEOSMinimumWidthTest.cpp
+++ b/tests/unit/capi/GEOSMinimumWidthTest.cpp
@@ -67,4 +67,16 @@ void object::test<2>
     GEOSGeom_destroy(output);
 }
 
+template<>
+template<>
+void object::test<3>
+()
+{
+    input_ = fromWKT("CIRCULARSTRING (0 0, 1 1, 2 0)");
+    ensure(input_);
+
+    result_ = GEOSMinimumWidth(input_);
+    ensure("curved geometry not supported", result_ == nullptr);
+}
+
 } // namespace tut
diff --git a/tests/unit/capi/GEOSNodeTest.cpp b/tests/unit/capi/GEOSNodeTest.cpp
index abda612bb..9b11e1a15 100644
--- a/tests/unit/capi/GEOSNodeTest.cpp
+++ b/tests/unit/capi/GEOSNodeTest.cpp
@@ -201,5 +201,17 @@ void object::test<8>
     GEOSFree(wkt_expected);
 }
 
+template<>
+template<>
+void object::test<9>()
+{
+    input_ = fromWKT("MULTICURVE (CIRCULARSTRING (0 0, 1 1, 2 0), CIRCULARSTRING (0 1, 1 0, 2 1))");
+    ensure(input_);
+
+    result_ = GEOSNode(input_);
+
+    ensure("curved geometries not supported", result_ == nullptr);
+}
+
 } // namespace tut
 
diff --git a/tests/unit/capi/GEOSOffsetCurveTest.cpp b/tests/unit/capi/GEOSOffsetCurveTest.cpp
index d5ddfb8f6..b3414dc15 100644
--- a/tests/unit/capi/GEOSOffsetCurveTest.cpp
+++ b/tests/unit/capi/GEOSOffsetCurveTest.cpp
@@ -259,5 +259,17 @@ void object::test<12>()
         );
 }
 
+template<>
+template<>
+void object::test<13>()
+{
+    input_ = fromWKT("CIRCULARSTRING (0 0, 1 1, 2 0)");
+    ensure(input_);
+
+    result_ = GEOSOffsetCurve(input_, 1, 8, GEOSBUF_JOIN_ROUND, 0);
+
+    ensure("curved geometries not supported", result_ == nullptr);
+}
+
 } // namespace tut
 
diff --git a/tests/unit/capi/GEOSOrientPolygonsTest.cpp b/tests/unit/capi/GEOSOrientPolygonsTest.cpp
index 745ac1bc0..1b9a7a3d1 100644
--- a/tests/unit/capi/GEOSOrientPolygonsTest.cpp
+++ b/tests/unit/capi/GEOSOrientPolygonsTest.cpp
@@ -90,5 +90,16 @@ void object::test<5>()
     ensure_equals(toWKT(geom1_), "GEOMETRYCOLLECTION (GEOMETRYCOLLECTION (MULTIPOLYGON (((0 0, 0 10, 10 10, 10 0, 0 0)))))");
 }
 
+template<>
+template<>
+void object::test<6>()
+{
+    input_ = fromWKT("CURVEPOLYGON (COMPOUNDCURVE( CIRCULARSTRING (0 0, 1 1, 2 0), (2 0, 0 0)))");
+    ensure(input_);
+
+    auto ret = GEOSOrientPolygons(input_, 0);
+    ensure_equals("curved geometries not supported", ret, -1);
+}
+
 } // namespace tut
 
diff --git a/tests/unit/capi/GEOSOverlapsTest.cpp b/tests/unit/capi/GEOSOverlapsTest.cpp
index 59cfa073e..253bd762f 100644
--- a/tests/unit/capi/GEOSOverlapsTest.cpp
+++ b/tests/unit/capi/GEOSOverlapsTest.cpp
@@ -35,5 +35,19 @@ void object::test<1>()
     ensure_equals(0, GEOSOverlaps(geom3_, geom2_));
 }
 
+template<>
+template<>
+void object::test<2>()
+{
+    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", GEOSOverlaps(geom1_, geom2_), 2);
+    ensure_equals("curved geometry not supported", GEOSOverlaps(geom2_, geom1_), 2);
+}
+
 } // namespace tut
 
diff --git a/tests/unit/capi/GEOSPointOnSurfaceTest.cpp b/tests/unit/capi/GEOSPointOnSurfaceTest.cpp
index 65f885e3d..2675a3a7c 100644
--- a/tests/unit/capi/GEOSPointOnSurfaceTest.cpp
+++ b/tests/unit/capi/GEOSPointOnSurfaceTest.cpp
@@ -224,5 +224,17 @@ void object::test<9>
     ensure_equals(std::string(wkt_), std::string("POINT (182755.892 141812.8789)"));
 }
 
+template<>
+template<>
+void object::test<10>
+()
+{
+    input_ = fromWKT("CURVEPOLYGON (COMPOUNDCURVE(CIRCULARSTRING (0 0, 1 1, 2 0), (2 0, 0 0)))");
+    ensure(input_);
+
+    result_ = GEOSPointOnSurface(input_);
+    ensure("curved geometry not supported", result_ == nullptr);
+}
+
 } // namespace tut
 
diff --git a/tests/unit/capi/GEOSPolygonHullSimplifyTest.cpp b/tests/unit/capi/GEOSPolygonHullSimplifyTest.cpp
index af98ebb30..43f426e04 100644
--- a/tests/unit/capi/GEOSPolygonHullSimplifyTest.cpp
+++ b/tests/unit/capi/GEOSPolygonHullSimplifyTest.cpp
@@ -93,6 +93,16 @@ void object::test<5>()
     ensure_geometry_equals(geom1_, expected_);
 }
 
+template<>
+template<>
+void object::test<6>()
+{
+    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);
+    ensure("curved geometries not supported", result_ == nullptr);
+}
 
 
 } // namespace tut
diff --git a/tests/unit/capi/GEOSPolygonizeTest.cpp b/tests/unit/capi/GEOSPolygonizeTest.cpp
index 695db3757..c0c2eb576 100644
--- a/tests/unit/capi/GEOSPolygonizeTest.cpp
+++ b/tests/unit/capi/GEOSPolygonizeTest.cpp
@@ -187,5 +187,28 @@ void object::test<6>
     GEOSGeom_destroy(expected_invalidRings);
 }
 
+template<>
+template<>
+void object::test<7>
+()
+{
+    constexpr int size = 2;
+    GEOSGeometry* geoms[size];
+    geoms[0] = GEOSGeomFromWKT("LINESTRING (0 0, 2 0)");
+    geoms[1] = GEOSGeomFromWKT("CIRCULARSTRING (0 0, 1 1, 2 0)");
+
+    for (auto& geom : geoms) {
+        ensure(geom != nullptr);
+    }
+
+    GEOSGeometry* g = GEOSPolygonize(geoms, size);
+
+    ensure("curved geometries not supported", g == nullptr);
+
+    for(auto& input : geoms) {
+        GEOSGeom_destroy(input);
+    }
+}
+
 } // namespace tut
 
diff --git a/tests/unit/capi/GEOSPreparedGeometryTest.cpp b/tests/unit/capi/GEOSPreparedGeometryTest.cpp
index 5d324d55c..4ee90cea5 100644
--- a/tests/unit/capi/GEOSPreparedGeometryTest.cpp
+++ b/tests/unit/capi/GEOSPreparedGeometryTest.cpp
@@ -441,5 +441,17 @@ void object::test<15>
 }
 
 
+template<>
+template<>
+void object::test<16>()
+{
+    geom1_ = fromWKT("CIRCULARSTRING (0 0, 1 1, 2 0)");
+
+    ensure(geom1_);
+
+    prepGeom1_ = GEOSPrepare(geom1_);
+    ensure("curved geometries not supported", prepGeom1_ == nullptr);
+}
+
 } // namespace tut
 
diff --git a/tests/unit/capi/GEOSRelatePatternTest.cpp b/tests/unit/capi/GEOSRelatePatternTest.cpp
index ef8e24f6e..1b6068749 100644
--- a/tests/unit/capi/GEOSRelatePatternTest.cpp
+++ b/tests/unit/capi/GEOSRelatePatternTest.cpp
@@ -31,5 +31,19 @@ void object::test<1>()
     ensure_equals(1, GEOSRelatePattern(geom1_, geom3_, "*FF*FF212"));
 }
 
+template<>
+template<>
+void object::test<11>()
+{
+    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);
+}
+
 } // namespace tut
 
diff --git a/tests/unit/capi/GEOSRelateTest.cpp b/tests/unit/capi/GEOSRelateTest.cpp
index 336c41f1e..f531ee4ab 100644
--- a/tests/unit/capi/GEOSRelateTest.cpp
+++ b/tests/unit/capi/GEOSRelateTest.cpp
@@ -30,5 +30,19 @@ void object::test<1>()
     GEOSFree(pattern);
 }
 
+template<>
+template<>
+void object::test<2>()
+{
+    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);
+}
+
 } // namespace tut
 
diff --git a/tests/unit/capi/GEOSRemoveRepeatedPointsTest.cpp b/tests/unit/capi/GEOSRemoveRepeatedPointsTest.cpp
index 8d6c9c2c6..1a0780b57 100644
--- a/tests/unit/capi/GEOSRemoveRepeatedPointsTest.cpp
+++ b/tests/unit/capi/GEOSRemoveRepeatedPointsTest.cpp
@@ -55,5 +55,16 @@ void object::test<2>
     ensure(result_ == nullptr);
 }
 
+template<>
+template<>
+void object::test<3>()
+{
+    input_ = fromWKT("CIRCULARSTRING (0 0, 1 1, 2 0, 2 0, 2 0)");
+    ensure(input_);
+
+    result_ = GEOSRemoveRepeatedPoints(input_, 0);
+
+    ensure("curved geometry not supported", result_ == nullptr);
+}
 
 } // namespace tut
diff --git a/tests/unit/capi/GEOSReverseTest.cpp b/tests/unit/capi/GEOSReverseTest.cpp
index 5f5b8020d..fae65d95f 100644
--- a/tests/unit/capi/GEOSReverseTest.cpp
+++ b/tests/unit/capi/GEOSReverseTest.cpp
@@ -123,4 +123,12 @@ void object::test<8>
     testReverse("GEOMETRYCOLLECTION EMPTY", "GEOMETRYCOLLECTION EMPTY");
 }
 
+template<>
+template<>
+void object::test<9>()
+{
+    testReverse("CIRCULARSTRING (0 0, 1 1, 2 0)",
+                "CIRCULARSTRING (2 0, 1 1, 0 0)");
+}
+
 } // namespace tut
diff --git a/tests/unit/capi/GEOSSharedPathsTest.cpp b/tests/unit/capi/GEOSSharedPathsTest.cpp
index 0c16640bc..db3b24fb9 100644
--- a/tests/unit/capi/GEOSSharedPathsTest.cpp
+++ b/tests/unit/capi/GEOSSharedPathsTest.cpp
@@ -88,5 +88,19 @@ void object::test<3>
     ensure(!geom3_);
 }
 
+template<>
+template<>
+void object::test<4>()
+{
+    geom1_ = fromWKT("CIRCULARSTRING (0 0, 1 1, 2 0)");
+    geom2_ = fromWKT("LINESTRING (1 0, 2 1)");
+
+    ensure(geom1_);
+    ensure(geom2_);
+
+    result_ = GEOSSharedPaths(geom1_, geom2_);
+    ensure("curved geometry not supported", result_ == nullptr);
+}
+
 } // namespace tut
 
diff --git a/tests/unit/capi/GEOSSimplifyTest.cpp b/tests/unit/capi/GEOSSimplifyTest.cpp
index 1067bbb77..407589b8b 100644
--- a/tests/unit/capi/GEOSSimplifyTest.cpp
+++ b/tests/unit/capi/GEOSSimplifyTest.cpp
@@ -44,5 +44,16 @@ void object::test<1>
     ensure(0 != GEOSisEmpty(geom2_));
 }
 
+template<>
+template<>
+void object::test<2>()
+{
+    input_ = fromWKT("CIRCULARSTRING (0 0, 1 1, 2 0)");
+    ensure(input_ != nullptr);
+
+    result_ = GEOSSimplify(input_, 2);
+    ensure(result_ == nullptr);
+}
+
 } // namespace tut
 
diff --git a/tests/unit/capi/GEOSSnapTest.cpp b/tests/unit/capi/GEOSSnapTest.cpp
index 17d9da8f2..fba18f8e8 100644
--- a/tests/unit/capi/GEOSSnapTest.cpp
+++ b/tests/unit/capi/GEOSSnapTest.cpp
@@ -209,5 +209,19 @@ void object::test<10>
     ensure_equals(out, "LINESTRING (-71.1257 42.2703, -71.1261 42.2703, -71.1261 42.2702, -71.1317 42.2509)");
 }
 
+template<>
+template<>
+void object::test<11>()
+{
+    geom1_ = fromWKT("CIRCULARSTRING (0 0, 1 1, 2 0)");
+    geom2_ = fromWKT("LINESTRING (1 1.0001, 2 1)");
+
+    ensure(geom1_);
+    ensure(geom2_);
+
+    result_ = GEOSSnap(geom1_, geom2_, 0.1);
+    ensure("curved geometry not supported", result_ == nullptr);
+}
+
 } // namespace tut
 
diff --git a/tests/unit/capi/GEOSSymDifferenceTest.cpp b/tests/unit/capi/GEOSSymDifferenceTest.cpp
index 34d2c0794..def092b68 100644
--- a/tests/unit/capi/GEOSSymDifferenceTest.cpp
+++ b/tests/unit/capi/GEOSSymDifferenceTest.cpp
@@ -29,5 +29,19 @@ void object::test<1>()
     ensure_equals("MULTILINESTRING ((50 150, 50 200), (50 50, 50 100))", toWKT(geom3_));
 }
 
+template<>
+template<>
+void object::test<2>()
+{
+    geom1_ = fromWKT("CIRCULARSTRING (0 0, 1 1, 2 0)");
+    geom2_ = fromWKT("LINESTRING (1 0, 2 1)");
+
+    ensure(geom1_);
+    ensure(geom2_);
+
+    result_ = GEOSSymDifference(geom1_, geom2_);
+    ensure("curved geometry not supported", result_ == nullptr);
+}
+
 } // namespace tut
 
diff --git a/tests/unit/capi/GEOSTopologyPreserveSimplifyTest.cpp b/tests/unit/capi/GEOSTopologyPreserveSimplifyTest.cpp
index 2df27d1a9..ca84ed899 100644
--- a/tests/unit/capi/GEOSTopologyPreserveSimplifyTest.cpp
+++ b/tests/unit/capi/GEOSTopologyPreserveSimplifyTest.cpp
@@ -52,5 +52,16 @@ void object::test<2>
     ensure_geometry_equals(result_, expected_);
 }
 
+template<>
+template<>
+void object::test<3>()
+{
+    input_ = fromWKT("CURVEPOLYGON (COMPOUNDCURVE (CIRCULARSTRING (0 0, 10 10, 20 0), (20 0, 10 0, 0 0)), (10 1, 11 1, 11 2, 10 1))");
+    ensure(input_);
+
+    result_ = GEOSTopologyPreserveSimplify(input_, 0.2);
+    ensure("curved geometry not supported", result_ == nullptr);
+}
+
 } // namespace tut
 
diff --git a/tests/unit/capi/GEOSTouchesTest.cpp b/tests/unit/capi/GEOSTouchesTest.cpp
index 79de79f62..df6722d5f 100644
--- a/tests/unit/capi/GEOSTouchesTest.cpp
+++ b/tests/unit/capi/GEOSTouchesTest.cpp
@@ -35,5 +35,19 @@ void object::test<1>()
     ensure_equals(0, GEOSTouches(geom3_, geom2_));
 }
 
+template<>
+template<>
+void object::test<2>()
+{
+    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", GEOSTouches(geom1_, geom2_), 2);
+    ensure_equals("curved geometry not supported", GEOSTouches(geom2_, geom1_), 2);
+}
+
 } // namespace tut
 
diff --git a/tests/unit/capi/GEOSUnaryUnionTest.cpp b/tests/unit/capi/GEOSUnaryUnionTest.cpp
index e164bbd39..b33b07a2f 100644
--- a/tests/unit/capi/GEOSUnaryUnionTest.cpp
+++ b/tests/unit/capi/GEOSUnaryUnionTest.cpp
@@ -218,6 +218,18 @@ void object::test<11>
     ensure_geometry_equals(result_, expected_);
 }
 
+template<>
+template<>
+void object::test<12>()
+{
+    input_ = fromWKT("CIRCULARSTRING (0 0, 1 1, 2 0)");
+
+    ensure(input_);
+
+    result_ = GEOSUnaryUnion(input_);
+    ensure("curved geometry not supported", result_ == nullptr);
+}
+
 
 } // namespace tut
 
diff --git a/tests/unit/capi/GEOSUnionTest.cpp b/tests/unit/capi/GEOSUnionTest.cpp
index 1053ec7a6..68c90867b 100644
--- a/tests/unit/capi/GEOSUnionTest.cpp
+++ b/tests/unit/capi/GEOSUnionTest.cpp
@@ -62,5 +62,19 @@ void object::test<2>()
     (void) result; // no crash
 }
 
+template<>
+template<>
+void object::test<3>()
+{
+    geom1_ = fromWKT("CIRCULARSTRING (0 0, 1 1, 2 0)");
+    geom2_ = fromWKT("LINESTRING (1 0, 2 1)");
+
+    ensure(geom1_);
+    ensure(geom2_);
+
+    result_ = GEOSUnion(geom1_, geom2_);
+    ensure("curved geometry not supported", result_ == nullptr);
+}
+
 } // namespace tut
 
diff --git a/tests/unit/capi/GEOSVoronoiDiagramTest.cpp b/tests/unit/capi/GEOSVoronoiDiagramTest.cpp
index c8843ac23..b379a49d0 100644
--- a/tests/unit/capi/GEOSVoronoiDiagramTest.cpp
+++ b/tests/unit/capi/GEOSVoronoiDiagramTest.cpp
@@ -175,4 +175,16 @@ void object::test<8>
     check_voronoi_ordered("LINESTRING (1 1, 2 2, 3 3)");
 }
 
+template<>
+template<>
+void object::test<9>
+()
+{
+    input_ = fromWKT("CIRCULARSTRING (0 0, 1 1, 2 0)");
+    ensure(input_);
+
+    result_ = GEOSVoronoiDiagram(input_, nullptr, 0, 0);
+    ensure("curved geometry not supported", result_ == nullptr);
+}
+
 } // namespace tut
diff --git a/tests/unit/capi/GEOSWKBWriterTest.cpp b/tests/unit/capi/GEOSWKBWriterTest.cpp
index 2f3e041d6..1bb95c808 100644
--- a/tests/unit/capi/GEOSWKBWriterTest.cpp
+++ b/tests/unit/capi/GEOSWKBWriterTest.cpp
@@ -186,5 +186,18 @@ void object::test<9>()
     ensure_equals(hexstr, "010100008000000000000008400000000000002040000000000000F03F");
 }
 
+template<>
+template<>
+void object::test<10>
+()
+{
+    input_ = fromWKT("CIRCULARSTRING (0 0, 1 1, 2 0)");
+    ensure(input_);
+
+    std::size_t hex_size = 0;
+    buf_ =  GEOSWKBWriter_writeHEX(wkbwriter_, input_, &hex_size);
+    ensure("curved geometry not supported", buf_ == nullptr);
+}
+
 } // namespace tut
 
diff --git a/tests/unit/capi/GEOSWithinTest.cpp b/tests/unit/capi/GEOSWithinTest.cpp
index c6ee8ede0..703a9b86e 100644
--- a/tests/unit/capi/GEOSWithinTest.cpp
+++ b/tests/unit/capi/GEOSWithinTest.cpp
@@ -88,5 +88,18 @@ void object::test<3>
     ensure_equals(int(r2), 1);
 }
 
+template<>
+template<>
+void object::test<4>()
+{
+    geom1_ = fromWKT("CIRCULARSTRING (0 0, 1 1, 2 0)");
+    geom2_ = fromWKT("LINESTRING (1 0, 2 0)");
+
+    ensure(geom1_);
+    ensure(geom2_);
+
+    ensure_equals("curved geometry not supported", GEOSWithin(geom2_, geom1_), 2);
+}
+
 } // namespace tut
 
diff --git a/tests/unit/capi/GEOSisClosedTest.cpp b/tests/unit/capi/GEOSisClosedTest.cpp
index c4becfdd7..656ec1ab1 100644
--- a/tests/unit/capi/GEOSisClosedTest.cpp
+++ b/tests/unit/capi/GEOSisClosedTest.cpp
@@ -36,7 +36,7 @@ void object::test<1>
 ()
 {
     geom1_ = GEOSGeomFromWKT("LINESTRING(0 0, 1 0, 1 1)");
-    int r = GEOSisClosed(geom1_);
+    char r = GEOSisClosed(geom1_);
     ensure_equals(r, 0);
 }
 
@@ -46,7 +46,7 @@ void object::test<2>
 ()
 {
     geom1_ = GEOSGeomFromWKT("LINESTRING(0 0, 0 1, 1 1, 0 0)");
-    int r = GEOSisClosed(geom1_);
+    char r = GEOSisClosed(geom1_);
     ensure_equals(r, 1);
 }
 
@@ -56,7 +56,7 @@ void object::test<3>
 ()
 {
     geom1_ = GEOSGeomFromWKT("MULTILINESTRING ((1 1, 1 2, 2 2, 1 1), (0 0, 0 1, 1 1))");
-    int r = GEOSisClosed(geom1_);
+    char r = GEOSisClosed(geom1_);
     ensure_equals(r, 0);
 }
 
@@ -66,7 +66,7 @@ void object::test<4>
 ()
 {
     geom1_ = GEOSGeomFromWKT("MULTILINESTRING ((1 1, 1 2, 2 2, 1 1), (0 0, 0 1, 1 1, 0 0))");
-    int r = GEOSisClosed(geom1_);
+    char r = GEOSisClosed(geom1_);
     ensure_equals(r, 1);
 }
 
@@ -76,8 +76,20 @@ void object::test<5>
 ()
 {
     geom1_ = GEOSGeomFromWKT("POINT (1 1)");
-    int r = GEOSisClosed(geom1_);
+    char r = GEOSisClosed(geom1_);
     ensure_equals(r, 2);
 }
 
+template<>
+template<>
+void object::test<6>
+()
+{
+    geom1_ = GEOSGeomFromWKT("COMPOUNDCURVE (CIRCULARSTRING (0 0, 1 1, 2 0), (2 0, 0 0))");
+    ensure(geom1_);
+
+    char r = GEOSisClosed(geom1_);
+    ensure_equals(r, 1);
+}
+
 } // namespace tut
diff --git a/tests/unit/capi/GEOSisRingTest.cpp b/tests/unit/capi/GEOSisRingTest.cpp
index 7337cd83e..1d0b6da47 100644
--- a/tests/unit/capi/GEOSisRingTest.cpp
+++ b/tests/unit/capi/GEOSisRingTest.cpp
@@ -36,7 +36,7 @@ void object::test<1>
 ()
 {
     geom1_ = GEOSGeomFromWKT("LINESTRING(0 0, 1 0, 1 1)");
-    int r = GEOSisRing(geom1_);
+    char r = GEOSisRing(geom1_);
     ensure_equals(r, 0);
 }
 
@@ -46,7 +46,7 @@ void object::test<2>
 ()
 {
     geom1_ = GEOSGeomFromWKT("LINESTRING (0 0, 1 0, 1 1, 0 0)");
-    int r = GEOSisRing(geom1_);
+    char r = GEOSisRing(geom1_);
     ensure_equals(r, 1);
 }
 
@@ -56,7 +56,7 @@ void object::test<3>
 ()
 {
     geom1_ = GEOSGeomFromWKT("POINT (1 1)");
-    int r = GEOSisRing(geom1_);
+    char r = GEOSisRing(geom1_);
     ensure_equals(r, 0);
 }
 
@@ -66,7 +66,7 @@ void object::test<4>
 ()
 {
     geom1_ = GEOSGeomFromWKT("MULTILINESTRING ((0 0, 1 0, 1 1, 0 0))");
-    int r = GEOSisRing(geom1_);
+    char r = GEOSisRing(geom1_);
     ensure_equals(r, 0);
 }
 
@@ -76,9 +76,21 @@ void object::test<5>
 ()
 {
     geom1_ = GEOSGeomFromWKT("LINESTRING EMPTY");
-    int r = GEOSisRing(geom1_);
+    char r = GEOSisRing(geom1_);
     ensure_equals(r, 0);
 }
 
 
+template<>
+template<>
+void object::test<6>
+()
+{
+    geom1_ = GEOSGeomFromWKT("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);
+}
+
 } // namespace tut
diff --git a/tests/unit/capi/GEOSisSimpleTest.cpp b/tests/unit/capi/GEOSisSimpleTest.cpp
index b25930dd9..762bacdb8 100644
--- a/tests/unit/capi/GEOSisSimpleTest.cpp
+++ b/tests/unit/capi/GEOSisSimpleTest.cpp
@@ -32,4 +32,15 @@ void object::test<2>()
     ensure_equals(0, GEOSisSimple(input_));
 }
 
+template<>
+template<>
+void object::test<3>()
+{
+    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);
+}
+
 } // namespace tut
diff --git a/tests/unit/capi/GEOSisValidDetailTest.cpp b/tests/unit/capi/GEOSisValidDetailTest.cpp
index cafa342f6..56d005268 100644
--- a/tests/unit/capi/GEOSisValidDetailTest.cpp
+++ b/tests/unit/capi/GEOSisValidDetailTest.cpp
@@ -144,5 +144,16 @@ void object::test<6>
     ensure_equals(r, 0); // invalid
 }
 
+template<>
+template<>
+void object::test<7>()
+{
+    input_ = fromWKT("CURVEPOLYGON (COMPOUNDCURVE( CIRCULARSTRING (0 0, 1 1, 2 0), (2 0, 1 1)))");
+    ensure(input_ != nullptr);
+
+    char ret = GEOSisValidDetail(input_, 0, nullptr, nullptr);
+    ensure_equals("error raised on curved geometry", ret, 2);
+}
+
 } // namespace tut
 
diff --git a/tests/unit/capi/GEOSisValidReasonTest.cpp b/tests/unit/capi/GEOSisValidReasonTest.cpp
index d253b7bfb..fbd988e59 100644
--- a/tests/unit/capi/GEOSisValidReasonTest.cpp
+++ b/tests/unit/capi/GEOSisValidReasonTest.cpp
@@ -57,6 +57,17 @@ void object::test<3>()
     GEOSFree(reason);
 }
 
+template<>
+template<>
+void object::test<7>()
+{
+    input_ = fromWKT("CURVEPOLYGON (COMPOUNDCURVE( CIRCULARSTRING (0 0, 1 1, 2 0), (2 0, 1 1)))");
+    ensure(input_ != nullptr);
+
+    char* reason = GEOSisValidReason(input_);
+    ensure(reason == nullptr);
+}
+
 
 } // namespace tut
 
diff --git a/tests/unit/capi/GEOSisValidTest.cpp b/tests/unit/capi/GEOSisValidTest.cpp
index a99684b79..a71fc5150 100644
--- a/tests/unit/capi/GEOSisValidTest.cpp
+++ b/tests/unit/capi/GEOSisValidTest.cpp
@@ -64,4 +64,15 @@ void object::test<3>()
     ensure_equals(2, isvalid);
 }
 
+template<>
+template<>
+void object::test<5>()
+{
+    input_ = fromWKT("CURVEPOLYGON (COMPOUNDCURVE( CIRCULARSTRING (0 0, 1 1, 2 0), (2 0, 1 1)))");
+    ensure(input_ != nullptr);
+
+    char ret = GEOSisValid(input_);
+    ensure_equals("error raised on curved geometry", ret, 2);
+}
+
 } // namespace tut
diff --git a/tests/unit/capi/capi_test_utils.h b/tests/unit/capi/capi_test_utils.h
index d6a9dc668..ef6f6c07b 100644
--- a/tests/unit/capi/capi_test_utils.h
+++ b/tests/unit/capi/capi_test_utils.h
@@ -82,7 +82,7 @@ namespace capitest {
         }
 
         std::string
-        toWKT(GEOSGeometry* g)
+        toWKT(const GEOSGeometry* g)
         {
             char* wkt = GEOSWKTWriter_write(wktw_, g);
             std::string ret(wkt);

commit 4fc18f5455244995827edde3adf258a5d820f0e8
Author: Daniel Baston <dbaston at gmail.com>
Date:   Wed Feb 21 13:32:52 2024 -0500

    Add tests for curved geometry types

diff --git a/include/geos/algorithm/ConvexHull.h b/include/geos/algorithm/ConvexHull.h
index 048d3aa71..df0f69ab6 100644
--- a/include/geos/algorithm/ConvexHull.h
+++ b/include/geos/algorithm/ConvexHull.h
@@ -33,6 +33,8 @@
 #include <geos/util/UniqueCoordinateArrayFilter.h>
 #include <geos/util/CoordinateArrayFilter.h>
 
+#include "geos/util.h"
+
 #ifdef _MSC_VER
 #pragma warning(push)
 #pragma warning(disable: 4251) // warning C4251: needs to have dll-interface to be used by clients of class
@@ -178,7 +180,9 @@ public:
     ConvexHull(const geom::Geometry* newGeometry)
         : inputGeom(newGeometry)
         , geomFactory(newGeometry->getFactory())
-    {};
+    {
+      util::ensureNotCurvedType(inputGeom);
+    };
 
     ~ConvexHull() {};
 
diff --git a/include/geos/geom/CircularString.h b/include/geos/geom/CircularString.h
index 3e7db7bbc..5a8734954 100644
--- a/include/geos/geom/CircularString.h
+++ b/include/geos/geom/CircularString.h
@@ -15,6 +15,7 @@
 #pragma once
 
 #include <geos/geom/SimpleCurve.h>
+#include <geos/util/UnsupportedOperationException.h>
 
 namespace geos {
 namespace geom {
@@ -47,6 +48,10 @@ public:
         return SORTINDEX_LINESTRING;
     };
 
+    double getLength() const override {
+        throw util::UnsupportedOperationException("Cannot calculate length of CircularString");
+    }
+
 };
 
 
diff --git a/include/geos/geom/CompoundCurve.h b/include/geos/geom/CompoundCurve.h
index e3a49804a..7228a144b 100644
--- a/include/geos/geom/CompoundCurve.h
+++ b/include/geos/geom/CompoundCurve.h
@@ -15,6 +15,7 @@
 #pragma once
 
 #include <geos/geom/SimpleCurve.h>
+#include <geos/util.h>
 #include <vector>
 
 namespace geos {
@@ -48,10 +49,6 @@ public:
 
     const SimpleCurve* getCurveN(std::size_t) const;
 
-    std::size_t getNumGeometries() const override;
-
-    const Geometry* getGeometryN(std::size_t) const override;
-
     std::size_t getNumPoints() const override;
 
     std::unique_ptr<Geometry> getBoundary() const override;
@@ -78,18 +75,15 @@ public:
 
     CompoundCurve* reverseImpl() const override;
 
+    double getLength() const override;
+
+    using Curve::apply_ro;
+    using Curve::apply_rw;
+
     void apply_rw(const CoordinateFilter* filter) override;
 
     void apply_ro(CoordinateFilter* filter) const override;
 
-    void apply_rw(GeometryFilter* filter) override;
-
-    void apply_ro(GeometryFilter* filter) const override;
-
-    void apply_rw(GeometryComponentFilter* filter) override;
-
-    void apply_ro(GeometryComponentFilter* filter) const override;
-
     void apply_rw(CoordinateSequenceFilter& filter) override;
 
     void apply_ro(CoordinateSequenceFilter& filter) const override;
@@ -113,7 +107,7 @@ protected:
         envelope = computeEnvelopeInternal();
     }
 
-    Envelope computeEnvelopeInternal();
+    Envelope computeEnvelopeInternal() const;
 
 private:
     std::vector<std::unique_ptr<SimpleCurve>> curves;
diff --git a/include/geos/geom/Curve.h b/include/geos/geom/Curve.h
index 10eea9580..520a4c299 100644
--- a/include/geos/geom/Curve.h
+++ b/include/geos/geom/Curve.h
@@ -36,6 +36,17 @@ public:
 
     virtual bool isClosed() const = 0;
 
+    using Geometry::apply_ro;
+    using Geometry::apply_rw;
+
+    void apply_rw(GeometryFilter* filter) override;
+
+    void apply_ro(GeometryFilter* filter) const override;
+
+    void apply_rw(GeometryComponentFilter* filter) override;
+
+    void apply_ro(GeometryComponentFilter* filter) const override;
+
 protected:
 
     Curve(const GeometryFactory& factory) : Geometry(&factory) {}
diff --git a/include/geos/geom/CurvePolygon.h b/include/geos/geom/CurvePolygon.h
index 548a092eb..9300df97f 100644
--- a/include/geos/geom/CurvePolygon.h
+++ b/include/geos/geom/CurvePolygon.h
@@ -41,6 +41,8 @@ public:
 
     void normalize() override;
 
+    double getArea() const override;
+
 protected:
     using SurfaceImpl::SurfaceImpl;
 
diff --git a/include/geos/geom/Geometry.h b/include/geos/geom/Geometry.h
index cf6cdd510..488332c1c 100644
--- a/include/geos/geom/Geometry.h
+++ b/include/geos/geom/Geometry.h
@@ -309,6 +309,11 @@ public:
     /// Return a string representation of this Geometry type
     virtual std::string getGeometryType() const = 0; //Abstract
 
+    /// Returns whether the Geometry type _may_ contain curved elements
+    virtual bool isCurvedType() const;
+
+    static bool isCurvedType(GeometryTypeId);
+
     /// Return an integer representation of this Geometry type
     virtual GeometryTypeId getGeometryTypeId() const = 0; //Abstract
 
diff --git a/include/geos/geom/Polygon.h b/include/geos/geom/Polygon.h
index 95936b52f..aabc4d866 100644
--- a/include/geos/geom/Polygon.h
+++ b/include/geos/geom/Polygon.h
@@ -100,9 +100,6 @@ public:
 
     double getArea() const override;
 
-    /// Returns the perimeter of this <code>Polygon</code>
-    double getLength() const override;
-
     bool isRectangle() const override;
 
     /**
diff --git a/include/geos/geom/SimpleCurve.h b/include/geos/geom/SimpleCurve.h
index 35e226ec8..240b54170 100644
--- a/include/geos/geom/SimpleCurve.h
+++ b/include/geos/geom/SimpleCurve.h
@@ -85,7 +85,7 @@ public:
 
     bool isRing() const;
 
-    virtual bool isCoordinate(Coordinate& pt) const;
+    virtual bool isCoordinate(CoordinateXY& pt) const;
 
     bool equalsExact(const Geometry* other, double tolerance = 0)
     const override;
@@ -99,18 +99,13 @@ public:
         return &envelope;
     }
 
+    using Curve::apply_ro;
+    using Curve::apply_rw;
+
     void apply_rw(const CoordinateFilter* filter) override;
 
     void apply_ro(CoordinateFilter* filter) const override;
 
-    void apply_rw(GeometryFilter* filter) override;
-
-    void apply_ro(GeometryFilter* filter) const override;
-
-    void apply_rw(GeometryComponentFilter* filter) override;
-
-    void apply_ro(GeometryComponentFilter* filter) const override;
-
     void apply_rw(CoordinateSequenceFilter& filter) override;
 
     void apply_ro(CoordinateSequenceFilter& filter) const override;
diff --git a/include/geos/geom/Surface.h b/include/geos/geom/Surface.h
index f6d66c8ee..46b491748 100644
--- a/include/geos/geom/Surface.h
+++ b/include/geos/geom/Surface.h
@@ -45,7 +45,7 @@ public:
 
 
     bool
-    equalsExact(const Geometry* other, double tolerance) const override;
+    equalsExact(const Geometry* other, double tolerance = 0.0) const override;
 
     bool
     equalsIdentical(const Geometry* other) const override;
@@ -75,6 +75,9 @@ public:
 
     bool isEmpty() const override;
 
+    /// Returns the perimeter of this Surface
+    double getLength() const override;
+
     void
     apply_ro(CoordinateFilter* filter) const override;
 
diff --git a/include/geos/geom/SurfaceImpl.h b/include/geos/geom/SurfaceImpl.h
index 0fd917dba..a69ecd0cd 100644
--- a/include/geos/geom/SurfaceImpl.h
+++ b/include/geos/geom/SurfaceImpl.h
@@ -108,8 +108,8 @@ public:
 
     /**
     * \brief
-    * Take ownership of this Polygon's exterior ring.
-    * After releasing the exterior ring, the Polygon should be
+    * Take ownership of this Surface's exterior ring.
+    * After releasing the exterior ring, the Surface should be
     * considered in a moved-from state and should not be accessed,
     * except to release the interior rings (if desired.)
     * @return exterior ring
@@ -139,14 +139,13 @@ public:
 
     /**
     * \brief
-    * Take ownership of this Polygon's interior rings.
-    * After releasing the rings, the Polygon should be
+    * Take ownership of this Surfaces's interior rings.
+    * After releasing the rings, the Surface should be
     * considered in a moved-from state and should not be accessed,
     * except to release the exterior ring (if desired.)
     * @return vector of rings (may be empty)
     */
-    std::vector<std::unique_ptr<RingType>>
-                                        releaseInteriorRings()
+    std::vector<std::unique_ptr<RingType>> releaseInteriorRings()
     {
         return std::move(holes);
     }
diff --git a/include/geos/operation/union/UnaryUnionOp.h b/include/geos/operation/union/UnaryUnionOp.h
index 35354ba61..6c62127b9 100644
--- a/include/geos/operation/union/UnaryUnionOp.h
+++ b/include/geos/operation/union/UnaryUnionOp.h
@@ -29,6 +29,8 @@
 #include <geos/geom/util/GeometryExtracter.h>
 #include <geos/operation/union/CascadedPolygonUnion.h>
 
+#include <geos/util.h>
+
 #ifdef _MSC_VER
 #pragma warning(push)
 #pragma warning(disable: 4251) // warning C4251: needs to have dll-interface to be used by clients of class
@@ -169,6 +171,8 @@ private:
     void
     extract(const geom::Geometry& geom)
     {
+        util::ensureNotCurvedType(geom);
+
         using namespace geom::util;
 
         if(! geomFact) {
diff --git a/include/geos/util.h b/include/geos/util.h
index 661dff451..bd2ff674d 100644
--- a/include/geos/util.h
+++ b/include/geos/util.h
@@ -24,6 +24,7 @@
 #include <cassert>
 #include <memory>
 #include <type_traits>
+#include <geos/util/UnsupportedOperationException.h>
 
 //
 // Private macros definition
@@ -59,6 +60,28 @@ template<typename To, typename From> inline To down_cast(From* f)
 }
 
 } // namespace detail
+
+namespace util {
+
+template<typename T>
+void ensureNotCurvedType(const T* geom)
+{
+    if (geom->isCurvedType()) {
+        throw UnsupportedOperationException("Curved geometry types are not supported.");
+    }
+}
+
+template<typename T>
+void ensureNotCurvedType(const T& geom)
+{
+    if (geom.isCurvedType()) {
+        throw UnsupportedOperationException("Curved geometry types are not supported.");
+    }
+}
+
+}
+
+
 } // namespace geos
 
 #endif // GEOS_UTIL_H
diff --git a/src/algorithm/Centroid.cpp b/src/algorithm/Centroid.cpp
index fa9355421..c99588da8 100644
--- a/src/algorithm/Centroid.cpp
+++ b/src/algorithm/Centroid.cpp
@@ -28,6 +28,8 @@
 
 #include <cmath> // for std::abs
 
+#include "geos/util.h"
+
 using namespace geos::geom;
 
 namespace geos {
@@ -68,6 +70,8 @@ Centroid::getCentroid(CoordinateXY& cent) const
 void
 Centroid::add(const Geometry& geom)
 {
+    util::ensureNotCurvedType(geom);
+
     if(geom.isEmpty()) {
         return;
     }
diff --git a/src/geom/CircularString.cpp b/src/geom/CircularString.cpp
index fa3962623..a4376fcb3 100644
--- a/src/geom/CircularString.cpp
+++ b/src/geom/CircularString.cpp
@@ -15,6 +15,7 @@
 #include <geos/geom/CircularString.h>
 #include <geos/geom/CoordinateSequence.h>
 #include <geos/geom/GeometryFactory.h>
+#include <geos/util/UnsupportedOperationException.h>
 
 namespace geos {
 namespace geom {
diff --git a/src/geom/CompoundCurve.cpp b/src/geom/CompoundCurve.cpp
index 40510dc7c..a5aa22552 100644
--- a/src/geom/CompoundCurve.cpp
+++ b/src/geom/CompoundCurve.cpp
@@ -12,6 +12,7 @@
  *
  **********************************************************************/
 
+#include <geos/geom/CoordinateFilter.h>
 #include <geos/geom/CompoundCurve.h>
 #include <geos/geom/GeometryFactory.h>
 #include <geos/operation/BoundaryOp.h>
@@ -23,7 +24,8 @@ namespace geom {
 CompoundCurve::CompoundCurve(std::vector<std::unique_ptr<SimpleCurve>>&& p_curves,
                              const GeometryFactory& gf)
     : Curve(gf),
-      curves(std::move(p_curves)) {}
+      curves(std::move(p_curves)),
+    envelope(computeEnvelopeInternal()) {}
 
 CompoundCurve::CompoundCurve(const CompoundCurve& other)
     : Curve(other),
@@ -130,24 +132,12 @@ CompoundCurve::getNumCurves() const
     return curves.size();
 }
 
-std::size_t
-CompoundCurve::getNumGeometries() const
-{
-    return curves.size();
-}
-
 const SimpleCurve*
 CompoundCurve::getCurveN(std::size_t i) const
 {
     return curves[i].get();
 }
 
-const Geometry*
-CompoundCurve::getGeometryN(std::size_t i) const
-{
-    return curves[i].get();
-}
-
 std::unique_ptr<Geometry>
 CompoundCurve::getBoundary() const
 {
@@ -232,16 +222,23 @@ CompoundCurve*
 CompoundCurve::reverseImpl() const
 {
     std::vector<std::unique_ptr<SimpleCurve>> reversed(curves.size());
-    std::transform(curves.rbegin(), curves.rend(), reversed.end(), [](const auto& curve) {
+    std::transform(curves.rbegin(), curves.rend(), reversed.begin(), [](const auto& curve) {
         return std::unique_ptr<SimpleCurve>(static_cast<SimpleCurve*>(curve->reverse().release()));
     });
 
     return getFactory()->createCompoundCurve(std::move(reversed)).release();
 }
 
+double CompoundCurve::getLength() const {
+    double sum = 0;
+    for (const auto& curve : curves) {
+        sum += curve->getLength();
+    }
+    return sum;
+}
+
 Envelope
-CompoundCurve::computeEnvelopeInternal()
-{
+CompoundCurve::computeEnvelopeInternal() const {
     Envelope e;
     for (const auto& curve : curves) {
         e.expandToInclude(curve->getEnvelopeInternal());
@@ -259,55 +256,43 @@ CompoundCurve::compareToSameClass(const Geometry* g) const
 void
 CompoundCurve::normalize()
 {
-    throw std::runtime_error("Not implemented.");
+    throw util::UnsupportedOperationException();
 }
 
 void
-CompoundCurve::apply_ro(GeometryFilter*) const
+CompoundCurve::apply_ro(CoordinateFilter* cf) const
 {
-    throw std::runtime_error("Not implemented.");
+    for (const auto& curve : curves) {
+        curve->apply_ro(cf);
+    }
 }
 
 void
-CompoundCurve::apply_ro(GeometryComponentFilter*) const
+CompoundCurve::apply_ro(CoordinateSequenceFilter& csf) const
 {
-    throw std::runtime_error("Not implemented.");
+    for (const auto& curve : curves) {
+        const auto& seq = *curve->getCoordinatesRO();
+        for (std::size_t i = 0; i < seq.size(); i++) {
+            if (csf.isDone()) {
+                return;
+            }
+            csf.filter_ro(seq, i);
+        }
+    }
 }
 
 void
-CompoundCurve::apply_ro(CoordinateFilter*) const
+CompoundCurve::apply_rw(const CoordinateFilter* cf)
 {
-    throw std::runtime_error("Not implemented.");
-}
-
-void
-CompoundCurve::apply_ro(CoordinateSequenceFilter&) const
-{
-    throw std::runtime_error("Not implemented.");
-}
-
-void
-CompoundCurve::apply_rw(GeometryFilter*)
-{
-    throw std::runtime_error("Not implemented.");
-}
-
-void
-CompoundCurve::apply_rw(GeometryComponentFilter*)
-{
-    throw std::runtime_error("Not implemented.");
-}
-
-void
-CompoundCurve::apply_rw(const CoordinateFilter*)
-{
-    throw std::runtime_error("Not implemented.");
+    for (auto& curve : curves) {
+        curve->apply_rw(cf);
+    }
 }
 
 void
 CompoundCurve::apply_rw(CoordinateSequenceFilter&)
 {
-    throw std::runtime_error("Not implemented.");
+    throw util::UnsupportedOperationException();
 }
 
 
diff --git a/src/geom/Curve.cpp b/src/geom/Curve.cpp
new file mode 100644
index 000000000..fd3013ec5
--- /dev/null
+++ b/src/geom/Curve.cpp
@@ -0,0 +1,52 @@
+/**********************************************************************
+ *
+ * GEOS - Geometry Engine Open Source
+ * http://geos.osgeo.org
+ *
+ * Copyright (C) 2011 Sandro Santilli <strk at kbt.io>
+ * Copyright (C) 2005-2006 Refractions Research Inc.
+ * Copyright (C) 2001-2002 Vivid Solutions Inc.
+ *
+ * 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/geom/Curve.h>
+#include <geos/geom/GeometryFilter.h>
+
+namespace geos {
+namespace geom {
+
+void
+Curve::apply_rw(GeometryFilter* filter)
+{
+    assert(filter);
+    filter->filter_rw(this);
+}
+
+void
+Curve::apply_ro(GeometryFilter* filter) const
+{
+    assert(filter);
+    filter->filter_ro(this);
+}
+
+void
+Curve::apply_rw(GeometryComponentFilter* filter)
+{
+    assert(filter);
+    filter->filter_rw(this);
+}
+
+void
+Curve::apply_ro(GeometryComponentFilter* filter) const
+{
+    assert(filter);
+    filter->filter_ro(this);
+}
+
+}
+}
\ No newline at end of file
diff --git a/src/geom/CurvePolygon.cpp b/src/geom/CurvePolygon.cpp
index bf542dd52..7afb1cb04 100644
--- a/src/geom/CurvePolygon.cpp
+++ b/src/geom/CurvePolygon.cpp
@@ -15,6 +15,7 @@
 #include <geos/geom/Curve.h>
 #include <geos/geom/CurvePolygon.h>
 #include <geos/geom/CoordinateSequence.h>
+#include <geos/util/UnsupportedOperationException.h>
 
 namespace geos {
 namespace geom {
@@ -24,13 +25,15 @@ namespace geom {
     {
         auto coordinates = shell->getCoordinates();
         for (const auto& hole : holes) {
-            // FIXME remove unncessary copy
-            coordinates->add(*hole->getCoordinates());
+            if (auto simpleHole = dynamic_cast<const SimpleCurve*>(hole.get())) {
+                coordinates->add(*simpleHole->getCoordinatesRO());
+            } else {
+                coordinates->add(*hole->getCoordinates());
+            }
         }
         return coordinates;
     }
 
-
     std::string CurvePolygon::getGeometryType() const {
         return "CurvePolygon";
     }
@@ -41,22 +44,31 @@ namespace geom {
 
     std::unique_ptr<Geometry>
     CurvePolygon::getBoundary() const {
-        throw std::runtime_error("Not implemented.");
+        throw util::UnsupportedOperationException();
     }
 
     void
     CurvePolygon::normalize() {
-        throw std::runtime_error("Not implemented.");
+        throw util::UnsupportedOperationException();
+    }
+
+    double CurvePolygon::getArea() const {
+        throw util::UnsupportedOperationException();
     }
 
     Geometry*
     CurvePolygon::cloneImpl() const {
-        throw std::runtime_error("Not implemented.");
+        return new CurvePolygon(*this);
     }
 
     Geometry*
     CurvePolygon::reverseImpl() const {
-        throw std::runtime_error("Not implemented.");
+        std::unique_ptr<Curve> revShell(static_cast<Curve*>(shell->reverse().release()));
+        std::vector<std::unique_ptr<Curve>> revHoles(holes.size());
+        for (std::size_t i = 0; i < revHoles.size(); i++) {
+            revHoles[i].reset(static_cast<Curve*>(holes[i]->reverse().release()));
+        }
+        return new CurvePolygon(std::move(revShell), std::move(revHoles), *getFactory());
     }
 
 }
diff --git a/src/geom/Geometry.cpp b/src/geom/Geometry.cpp
index eb15b0149..c6c6f766d 100644
--- a/src/geom/Geometry.cpp
+++ b/src/geom/Geometry.cpp
@@ -796,6 +796,25 @@ Geometry::getPrecisionModel() const
     return _factory->getPrecisionModel();
 }
 
+bool
+Geometry::isCurvedType(GeometryTypeId typ) {
+    switch(typ) {
+        case GEOS_CIRCULARSTRING:
+        case GEOS_COMPOUNDCURVE:
+        case GEOS_CURVEPOLYGON:
+        case GEOS_MULTICURVE:
+        case GEOS_MULTISURFACE:
+            return true;
+        default:
+            return false;
+    }
+}
+
+bool
+Geometry::isCurvedType() const {
+    return isCurvedType(getGeometryTypeId());
+}
+
 } // namespace geos::geom
 } // namespace geos
 
diff --git a/src/geom/GeometryFactory.cpp b/src/geom/GeometryFactory.cpp
index a143da299..4e97f85cd 100644
--- a/src/geom/GeometryFactory.cpp
+++ b/src/geom/GeometryFactory.cpp
@@ -668,7 +668,8 @@ std::unique_ptr<CompoundCurve>
 GeometryFactory::createCompoundCurve()
 const
 {
-    return std::unique_ptr<CompoundCurve>();
+    std::vector<std::unique_ptr<SimpleCurve>> curves;
+    return createCompoundCurve(std::move(curves));
 }
 
 /*public*/
diff --git a/src/geom/Polygon.cpp b/src/geom/Polygon.cpp
index 938a1786d..3163e8543 100644
--- a/src/geom/Polygon.cpp
+++ b/src/geom/Polygon.cpp
@@ -153,23 +153,6 @@ Polygon::getArea() const
     return area;
 }
 
-/**
- * Returns the perimeter of this <code>Polygon</code>
- *
- * @return the perimeter of the polygon
- */
-double
-Polygon::getLength() const
-{
-    double len = 0.0;
-    len += shell->getLength();
-    for(const auto& hole : holes) {
-        len += hole->getLength();
-    }
-    return len;
-}
-
-
 GeometryTypeId
 Polygon::getGeometryTypeId() const
 {
diff --git a/src/geom/SimpleCurve.cpp b/src/geom/SimpleCurve.cpp
index 9fc75e1af..471d48ff8 100644
--- a/src/geom/SimpleCurve.cpp
+++ b/src/geom/SimpleCurve.cpp
@@ -45,6 +45,16 @@ SimpleCurve::SimpleCurve(std::unique_ptr<CoordinateSequence>&& newCoords,
 {
 }
 
+Envelope
+SimpleCurve::computeEnvelopeInternal() const
+{
+    if(isEmpty()) {
+        return Envelope();
+    }
+
+    return points->getEnvelope();
+}
+
 std::unique_ptr<CoordinateSequence>
 SimpleCurve::getCoordinates() const
 {
@@ -169,7 +179,7 @@ SimpleCurve::getBoundary() const
 }
 
 bool
-SimpleCurve::isCoordinate(Coordinate& pt) const
+SimpleCurve::isCoordinate(CoordinateXY& pt) const
 {
     assert(points.get());
     std::size_t npts = points->getSize();
@@ -181,17 +191,6 @@ SimpleCurve::isCoordinate(Coordinate& pt) const
     return false;
 }
 
-/*protected*/
-Envelope
-SimpleCurve::computeEnvelopeInternal() const
-{
-    if(isEmpty()) {
-        return Envelope();
-    }
-
-    return points->getEnvelope();
-}
-
 const CoordinateXY*
 SimpleCurve::getCoordinate() const
 {
@@ -293,6 +292,8 @@ SimpleCurve::normalizeClosed()
 void
 SimpleCurve::normalize()
 {
+    util::ensureNotCurvedType(*this);
+
     if (isEmpty()) return;
     assert(points.get());
     if (isClosed()) {
@@ -327,33 +328,6 @@ SimpleCurve::apply_ro(CoordinateFilter* filter) const
     points->apply_ro(filter);
 }
 
-void
-SimpleCurve::apply_rw(GeometryFilter* filter)
-{
-    assert(filter);
-    filter->filter_rw(this);
-}
-
-void
-SimpleCurve::apply_ro(GeometryFilter* filter) const
-{
-    assert(filter);
-    filter->filter_ro(this);
-}
-
-void
-SimpleCurve::apply_rw(GeometryComponentFilter* filter)
-{
-    assert(filter);
-    filter->filter_rw(this);
-}
-
-void
-SimpleCurve::apply_ro(GeometryComponentFilter* filter) const
-{
-    assert(filter);
-    filter->filter_ro(this);
-}
 
 void
 SimpleCurve::apply_rw(CoordinateSequenceFilter& filter)
diff --git a/src/geom/Surface.cpp b/src/geom/Surface.cpp
index c3ec61346..343dfb1e8 100644
--- a/src/geom/Surface.cpp
+++ b/src/geom/Surface.cpp
@@ -266,6 +266,15 @@ Surface::isEmpty() const
     return getExteriorRing()->isEmpty();
 }
 
+double Surface::getLength() const {
+    double len = 0.0;
+    len += getExteriorRing()->getLength();
+    for(std::size_t i = 0; i < getNumInteriorRing(); i++) {
+        len += getInteriorRingN(i)->getLength();
+    }
+    return len;
+}
+
 std::unique_ptr<Geometry>
 Surface::createEmptyRing(const GeometryFactory& factory)
 {
diff --git a/src/geom/prep/BasicPreparedGeometry.cpp b/src/geom/prep/BasicPreparedGeometry.cpp
index efde4ce2a..3dc08f569 100644
--- a/src/geom/prep/BasicPreparedGeometry.cpp
+++ b/src/geom/prep/BasicPreparedGeometry.cpp
@@ -23,6 +23,8 @@
 #include <geos/geom/util/ComponentCoordinateExtracter.h>
 #include <geos/operation/distance/DistanceOp.h>
 
+#include "geos/util.h"
+
 namespace geos {
 namespace geom { // geos.geom
 namespace prep { // geos.geom.prep
diff --git a/src/geom/prep/PreparedGeometryFactory.cpp b/src/geom/prep/PreparedGeometryFactory.cpp
index 899eff9a3..565e9e21e 100644
--- a/src/geom/prep/PreparedGeometryFactory.cpp
+++ b/src/geom/prep/PreparedGeometryFactory.cpp
@@ -45,6 +45,8 @@ PreparedGeometryFactory::create(const geom::Geometry* g) const
         throw util::IllegalArgumentException("PreparedGeometry constructed with null Geometry object");
     }
 
+    util::ensureNotCurvedType(g);
+
     std::unique_ptr<PreparedGeometry> pg;
 
     switch(g->getGeometryTypeId()) {
diff --git a/src/operation/BoundaryOp.cpp b/src/operation/BoundaryOp.cpp
index 504861784..acc4a316a 100644
--- a/src/operation/BoundaryOp.cpp
+++ b/src/operation/BoundaryOp.cpp
@@ -52,6 +52,8 @@ BoundaryOp::BoundaryOp(const geom::Geometry& geom, const algorithm::BoundaryNode
 std::unique_ptr<geom::Geometry>
 BoundaryOp::getBoundary()
 {
+    util::ensureNotCurvedType(m_geom);
+
     if (auto ls = dynamic_cast<const LineString*>(&m_geom)) {
         return boundaryLineString(*ls);
     }
diff --git a/src/operation/distance/DistanceOp.cpp b/src/operation/distance/DistanceOp.cpp
index 952c68d96..5ca7f941e 100644
--- a/src/operation/distance/DistanceOp.cpp
+++ b/src/operation/distance/DistanceOp.cpp
@@ -104,6 +104,9 @@ DistanceOp::distance()
 {
     using geos::util::IllegalArgumentException;
 
+    util::ensureNotCurvedType(geom[0]);
+    util::ensureNotCurvedType(geom[1]);
+
     if(geom[0] == nullptr || geom[1] == nullptr) {
         throw IllegalArgumentException("null geometries are not supported");
     }
diff --git a/src/operation/overlayng/OverlayNGRobust.cpp b/src/operation/overlayng/OverlayNGRobust.cpp
index 016eeb3df..2c7604014 100644
--- a/src/operation/overlayng/OverlayNGRobust.cpp
+++ b/src/operation/overlayng/OverlayNGRobust.cpp
@@ -83,6 +83,9 @@ OverlayNGRobust::Union(const Geometry* a)
 std::unique_ptr<Geometry>
 OverlayNGRobust::Overlay(const Geometry* geom0, const Geometry* geom1, int opCode)
 {
+    geos::util::ensureNotCurvedType(geom0);
+    geos::util::ensureNotCurvedType(geom1);
+
     std::unique_ptr<Geometry> result;
     std::runtime_error exOriginal("");
 
diff --git a/src/operation/union/UnaryUnionOp.cpp b/src/operation/union/UnaryUnionOp.cpp
index 8bd96b847..a801e5658 100644
--- a/src/operation/union/UnaryUnionOp.cpp
+++ b/src/operation/union/UnaryUnionOp.cpp
@@ -35,6 +35,8 @@
 #include <geos/geom/util/GeometryCombiner.h>
 #include <geos/algorithm/PointLocator.h>
 
+#include "geos/util.h"
+
 namespace geos {
 namespace operation { // geos::operation
 namespace geounion {  // geos::operation::geounion
diff --git a/src/operation/valid/IsSimpleOp.cpp b/src/operation/valid/IsSimpleOp.cpp
index 61e966b4c..065cd18d8 100644
--- a/src/operation/valid/IsSimpleOp.cpp
+++ b/src/operation/valid/IsSimpleOp.cpp
@@ -115,24 +115,27 @@ IsSimpleOp::computeSimple(const Geometry& geom)
 {
     if (geom.isEmpty()) return true;
     switch(geom.getGeometryTypeId()) {
+        case GEOS_POINT:
+            return true;
         case GEOS_MULTIPOINT:
             return isSimpleMultiPoint(dynamic_cast<const MultiPoint&>(geom));
         case GEOS_LINESTRING:
-            return isSimpleLinearGeometry(geom);
         case GEOS_MULTILINESTRING:
             return isSimpleLinearGeometry(geom);
         case GEOS_LINEARRING:
-            return isSimplePolygonal(geom);
         case GEOS_POLYGON:
-            return isSimplePolygonal(geom);
         case GEOS_MULTIPOLYGON:
             return isSimplePolygonal(geom);
         case GEOS_GEOMETRYCOLLECTION:
             return isSimpleGeometryCollection(geom);
-        // all other geometry types are simple by definition
-        default:
-            return true;
+        case GEOS_CIRCULARSTRING:
+        case GEOS_COMPOUNDCURVE:
+        case GEOS_MULTICURVE:
+        case GEOS_CURVEPOLYGON:
+        case GEOS_MULTISURFACE:
+            throw util::UnsupportedOperationException("Curved types not supported in IsSimpleOp.");
     }
+    throw util::UnsupportedOperationException("Unexpected type.");
 }
 
 /* private */
diff --git a/src/operation/valid/IsValidOp.cpp b/src/operation/valid/IsValidOp.cpp
index 813ab8454..454d9a18c 100644
--- a/src/operation/valid/IsValidOp.cpp
+++ b/src/operation/valid/IsValidOp.cpp
@@ -107,7 +107,7 @@ IsValidOp::isValidGeometry(const Geometry* g)
         case GEOS_CURVEPOLYGON:
         case GEOS_MULTICURVE:
         case GEOS_MULTISURFACE:
-            throw util::IllegalArgumentException("Curved types not supported in IsValidOp.");
+            throw util::UnsupportedOperationException("Curved types not supported in IsValidOp.");
     }
 
     // geometry type not known
diff --git a/tests/unit/geom/CircularStringTest.cpp b/tests/unit/geom/CircularStringTest.cpp
new file mode 100644
index 000000000..6f5cf03ae
--- /dev/null
+++ b/tests/unit/geom/CircularStringTest.cpp
@@ -0,0 +1,178 @@
+#include <tut/tut.hpp>
+#include <tut/tut_macros.hpp>
+
+#include <geos/geom/CircularString.h>
+#include <geos/geom/GeometryFactory.h>
+#include <geos/geom/CoordinateSequence.h>
+#include <geos/geom/IntersectionMatrix.h>
+#include <geos/io/WKTReader.h>
+#include <geos/util/UnsupportedOperationException.h>
+
+using geos::geom::CoordinateSequence;
+using geos::geom::CircularString;
+using XY = geos::geom::CoordinateXY;
+
+namespace tut {
+// Common data used by tests
+struct test_circularstring_data {
+
+    geos::geom::GeometryFactory::Ptr factory_ = geos::geom::GeometryFactory::create();
+    geos::io::WKTReader wktreader_;
+
+    std::unique_ptr<CircularString> cs_;
+
+    test_circularstring_data()
+    {
+        CoordinateSequence seq{
+            XY(0, 0),
+            XY(1, 1),
+            XY(2, 0),
+            XY(3, -1),
+            XY(4, 0)
+        };
+
+        cs_ = factory_->createCircularString(seq);
+    }
+};
+
+typedef test_group<test_circularstring_data> group;
+typedef group::object object;
+
+group test_circularstring_group("geos::geom::CircularString");
+
+template<>
+template<>
+void object::test<1>()
+{
+
+    auto cs = factory_->createCircularString(false, false);
+
+    ensure(cs->isEmpty());
+    ensure_equals(cs->getNumPoints(), 0u);
+    ensure(!cs->hasZ());
+    ensure(!cs->hasM());
+    ensure_equals(cs->getCoordinateDimension(), 2u);
+
+    ensure(cs->getCoordinatesRO()->isEmpty());
+    ensure(cs->getCoordinates()->isEmpty());
+    ensure(cs->getCoordinate() == nullptr);
+
+    ensure_equals(cs->getArea(), 0);
+    ensure_THROW(cs_->getLength(), geos::util::UnsupportedOperationException);
+}
+
+// Basic Geometry API
+template<>
+template<>
+void object::test<2>()
+{
+    // Geometry type functions
+    ensure_equals("getGeometryType", cs_->getGeometryType(), "CircularString");
+    ensure_equals("getGeometryTypdId", cs_->getGeometryTypeId(), geos::geom::GEOS_CIRCULARSTRING);
+    ensure("isCollection", !cs_->isCollection());
+
+    // Geometry size functions
+    ensure("isEmpty", !cs_->isEmpty());
+    ensure_equals("getArea", cs_->getArea(), 0);
+    ensure_THROW(cs_->getLength(), geos::util::UnsupportedOperationException);
+    ensure_equals("getNumGeometries", cs_->getNumGeometries(), 1u);
+    ensure_equals("getNumPoints", cs_->getNumPoints(), 5u);
+    ensure(!cs_->getEnvelopeInternal()->isNull());
+    // FIXME calculate envelope correctly
+
+    // Geometry dimension functions
+    ensure_equals("getDimension", cs_->getDimension(), geos::geom::Dimension::L);
+    ensure("isLineal", cs_->isLineal());
+    ensure("isPuntal", !cs_->isPuntal());
+    ensure("isPolygonal", !cs_->isPolygonal());
+    ensure("hasDimension(L)", cs_->hasDimension(geos::geom::Dimension::L));
+    ensure("hasDimension(P)", !cs_->hasDimension(geos::geom::Dimension::P));
+    ensure("hasDimension(A)", !cs_->hasDimension(geos::geom::Dimension::A));
+    ensure("isDimensionStrict", cs_->isDimensionStrict(geos::geom::Dimension::L));
+    ensure("isMixedDimension", !cs_->isMixedDimension());
+    ensure_equals("getBoundaryDimension", cs_->getBoundaryDimension(), geos::geom::Dimension::P);
+
+    // Coordinate dimension functions
+    ensure("hasZ", !cs_->hasZ());
+    ensure("hasM", !cs_->hasM());
+    ensure_equals("getCoordinateDimension", cs_->getCoordinateDimension(), 2u);
+
+    // Coordinate access functions
+    ensure("getCoordinates", cs_->getCoordinates()->getSize() == 5u);
+    ensure_equals("getCoordinate", *cs_->getCoordinate(), XY(0, 0));
+}
+
+// Operations
+template<>
+template<>
+void object::test<3>()
+{
+    // Predicates
+    ensure_THROW(cs_->contains(cs_.get()), geos::util::UnsupportedOperationException);
+    ensure_THROW(cs_->coveredBy(cs_.get()), geos::util::UnsupportedOperationException);
+    ensure_THROW(cs_->covers(cs_.get()), geos::util::UnsupportedOperationException);
+    ensure_THROW(cs_->crosses(cs_.get()), geos::util::UnsupportedOperationException);
+    ensure_THROW(cs_->disjoint(cs_.get()), geos::util::UnsupportedOperationException);
+    ensure_THROW(cs_->equals(cs_.get()), geos::util::UnsupportedOperationException);
+    ensure_THROW(cs_->intersects(cs_.get()), geos::util::UnsupportedOperationException);
+    ensure_THROW(cs_->overlaps(cs_.get()), geos::util::UnsupportedOperationException);
+    ensure_THROW(cs_->relate(cs_.get()), geos::util::UnsupportedOperationException);
+    ensure_THROW(cs_->touches(cs_.get()), geos::util::UnsupportedOperationException);
+    ensure_THROW(cs_->within(cs_.get()), geos::util::UnsupportedOperationException);
+
+    auto cs2 = cs_->clone();
+
+    ensure("equalsExact", cs_->equalsExact(cs2.get()));
+    ensure("equalsIdentical", cs_->equalsIdentical(cs2.get()));
+
+    // Overlay
+    ensure_THROW(cs_->Union(), geos::util::UnsupportedOperationException);
+    ensure_THROW(cs_->Union(cs_.get()), geos::util::UnsupportedOperationException);
+    ensure_THROW(cs_->difference(cs_.get()), geos::util::UnsupportedOperationException);
+    ensure_THROW(cs_->intersection(cs_.get()), geos::util::UnsupportedOperationException);
+    ensure_THROW(cs_->symDifference(cs_.get()), geos::util::UnsupportedOperationException);
+
+    // Distance
+    ensure_THROW(cs_->distance(cs_.get()), geos::util::UnsupportedOperationException);
+    ensure_THROW(cs_->isWithinDistance(cs_.get(), 1), geos::util::UnsupportedOperationException);
+
+    // Valid / Simple
+    ensure_THROW(cs_->isSimple(), geos::util::UnsupportedOperationException);
+    ensure_THROW(cs_->isValid(), geos::util::UnsupportedOperationException);
+
+    // Operations
+    ensure_THROW(cs_->convexHull(), geos::util::UnsupportedOperationException);
+    ensure_THROW(cs_->buffer(1), geos::util::UnsupportedOperationException);
+
+    ensure_THROW(cs_->getCentroid(), geos::util::UnsupportedOperationException);
+
+    //auto expected_boundary = wktreader_.read("MULTIPOINT ((0 0), (1 1), (2 0), (3 -1), (4 0))");
+    //ensure("getBoundary", cs_->getBoundary()->equalsIdentical(expected_boundary.get()));
+    ensure_THROW(cs_->getBoundary(), geos::util::UnsupportedOperationException);
+
+    ensure("clone", cs_->equalsIdentical(cs_->clone().get()));
+
+    ensure("reverse", cs_->reverse()->equalsIdentical(wktreader_.read("CIRCULARSTRING (4 0, 3 -1, 2 0, 1 1, 0 0)").get()));
+
+    auto cs3 = cs_->reverse();
+    ensure_THROW(cs3->normalize(), geos::util::UnsupportedOperationException);
+}
+
+// SimpleCurve API
+template<>
+template<>
+void object::test<4>()
+{
+    ensure("getCoordinateN", cs_->getCoordinateN(3).equals(XY(3, -1)));
+    ensure("getPointN", cs_->getPointN(1)->equalsIdentical(wktreader_.read("POINT (1 1)").get()));
+
+    ensure("getStartPoint", cs_->getStartPoint()->equalsIdentical(wktreader_.read("POINT (0 0)").get()));
+    ensure("getEndPoint", cs_->getEndPoint()->equalsIdentical(wktreader_.read("POINT (4 0)").get()));
+
+    ensure("getCoordinatesRO", cs_->getCoordinatesRO()->getSize() == 5u);
+    ensure("isClosed", !cs_->isClosed());
+    XY pt(4, 0);
+    ensure("isCoordinate", cs_->isCoordinate(pt));
+}
+
+}
diff --git a/tests/unit/geom/CompoundCurveTest.cpp b/tests/unit/geom/CompoundCurveTest.cpp
new file mode 100644
index 000000000..c394bf945
--- /dev/null
+++ b/tests/unit/geom/CompoundCurveTest.cpp
@@ -0,0 +1,318 @@
+#include <utility.h>
+#include <tut/tut.hpp>
+#include <tut/tut_macros.hpp>
+
+#include <geos/geom/Coordinate.h>
+#include <geos/geom/CoordinateFilter.h>
+#include <geos/geom/GeometryFactory.h>
+#include <geos/geom/CompoundCurve.h>
+#include <geos/geom/CircularString.h>
+#include <geos/geom/IntersectionMatrix.h>
+#include <geos/io/WKTReader.h>
+#include <geos/util.h>
+
+using geos::geom::CoordinateXY;
+using geos::geom::CoordinateSequence;
+
+namespace tut {
+// Common data used by tests
+struct test_compoundcurve_data {
+
+    geos::geom::GeometryFactory::Ptr factory_ = geos::geom::GeometryFactory::create();
+    geos::io::WKTReader wktreader_;
+
+    std::unique_ptr<geos::geom::CompoundCurve> cc_;
+
+    test_compoundcurve_data()
+    {
+        std::vector<std::unique_ptr<geos::geom::SimpleCurve>> curves;
+
+        curves.emplace_back(factory_->createCircularString({
+            CoordinateXY(0, 0),
+            CoordinateXY(1, 1),
+            CoordinateXY(2, 0)
+        }));
+
+        curves.emplace_back(factory_->createLineString({
+            CoordinateXY(2, 0),
+            CoordinateXY(2, 2)
+        }));
+
+        cc_ = factory_->createCompoundCurve(std::move(curves));
+    }
+
+};
+
+typedef test_group<test_compoundcurve_data> group;
+typedef group::object object;
+
+group test_compoundcurve_group("geos::geom::CompoundCurve");
+
+template<>
+template<>
+void object::test<1>()
+{
+    auto cc = factory_->createCompoundCurve();
+
+    ensure("isEmpty", cc->isEmpty());
+    ensure_equals("getNumPoints", cc->getNumPoints(), 0u);
+    ensure_equals("getNumCurves", cc->getNumCurves(), 0u);
+    ensure("hasZ", !cc->hasZ());
+    ensure("hasM", !cc->hasM());
+    ensure_equals("getCoordinateDimension", cc->getCoordinateDimension(), 2u);
+
+    ensure("getCoordinates", cc->getCoordinates()->isEmpty());
+    ensure("getCoordinate", cc->getCoordinate() == nullptr);
+
+    ensure_equals("getArea", cc->getArea(), 0);
+    ensure_THROW(cc_->getLength(), geos::util::UnsupportedOperationException);
+}
+
+// Basic Geometry API
+template<>
+template<>
+void object::test<2>()
+{
+    // Geometry type functions
+    ensure_equals("getGeometryType", cc_->getGeometryType(), "CompoundCurve");
+    ensure_equals("getGeometryTypdId", cc_->getGeometryTypeId(), geos::geom::GEOS_COMPOUNDCURVE);
+    ensure("isCollection", !cc_->isCollection());
+
+    // Geometry size functions
+    ensure("isEmpty", !cc_->isEmpty());
+    ensure_equals("getArea", cc_->getArea(), 0);
+    ensure_THROW(cc_->getLength(), geos::util::UnsupportedOperationException);
+    ensure_equals("getNumGeometries", cc_->getNumGeometries(), 1u);
+    ensure_equals("getNumCurves", cc_->getNumCurves(), 2u);
+    ensure_equals("getNumPoints", cc_->getNumPoints(), 5u); // FIXME should this be 5 or 4?
+    ensure(!cc_->getEnvelopeInternal()->isNull());
+
+    // Geometry dimension functions
+    ensure_equals("getDimension", cc_->getDimension(), geos::geom::Dimension::L);
+    ensure("isLineal", cc_->isLineal());
+    ensure("isPuntal", !cc_->isPuntal());
+    ensure("isPolygonal", !cc_->isPolygonal());
+    ensure("hasDimension(L)", cc_->hasDimension(geos::geom::Dimension::L));
+    ensure("hasDimension(P)", !cc_->hasDimension(geos::geom::Dimension::P));
+    ensure("hasDimension(A)", !cc_->hasDimension(geos::geom::Dimension::A));
+    ensure("isDimensionStrict", cc_->isDimensionStrict(geos::geom::Dimension::L));
+    ensure("isMixedDimension", !cc_->isMixedDimension());
+    ensure_equals("getBoundaryDimension", cc_->getBoundaryDimension(), geos::geom::Dimension::P);
+
+    // Coordinate dimension functions
+    ensure("hasZ", !cc_->hasZ());
+    ensure("hasM", !cc_->hasM());
+    ensure_equals("getCoordinateDimension", cc_->getCoordinateDimension(), 2u);
+
+    // Coordinate access functions
+    ensure("getCoordinates", cc_->getCoordinates()->getSize() == 5u);
+    ensure_equals("getCoordinate", *cc_->getCoordinate(), CoordinateXY(0, 0));
+}
+
+// Operations
+template<>
+template<>
+void object::test<3>()
+{
+    // Predicates
+    ensure_THROW(cc_->contains(cc_.get()), geos::util::UnsupportedOperationException);
+    ensure_THROW(cc_->coveredBy(cc_.get()), geos::util::UnsupportedOperationException);
+    ensure_THROW(cc_->covers(cc_.get()), geos::util::UnsupportedOperationException);
+    ensure_THROW(cc_->crosses(cc_.get()), geos::util::UnsupportedOperationException);
+    ensure_THROW(cc_->disjoint(cc_.get()), geos::util::UnsupportedOperationException);
+    ensure_THROW(cc_->equals(cc_.get()), geos::util::UnsupportedOperationException);
+    ensure_THROW(cc_->intersects(cc_.get()), geos::util::UnsupportedOperationException);
+    ensure_THROW(cc_->overlaps(cc_.get()), geos::util::UnsupportedOperationException);
+    ensure_THROW(cc_->relate(cc_.get()), geos::util::UnsupportedOperationException);
+    ensure_THROW(cc_->touches(cc_.get()), geos::util::UnsupportedOperationException);
+    ensure_THROW(cc_->within(cc_.get()), geos::util::UnsupportedOperationException);
+
+    auto cc2 = cc_->clone();
+
+    ensure("equalsExact", cc_->equalsExact(cc2.get()));
+    ensure("equalsIdentical", cc_->equalsIdentical(cc2.get()));
+
+    // Overlay
+    ensure_THROW(cc_->Union(), geos::util::UnsupportedOperationException);
+    ensure_THROW(cc_->Union(cc_.get()), geos::util::UnsupportedOperationException);
+    ensure_THROW(cc_->difference(cc_.get()), geos::util::UnsupportedOperationException);
+    ensure_THROW(cc_->intersection(cc_.get()), geos::util::UnsupportedOperationException);
+    ensure_THROW(cc_->symDifference(cc_.get()), geos::util::UnsupportedOperationException);
+
+    // Distance
+    ensure_THROW(cc_->distance(cc_.get()), geos::util::UnsupportedOperationException);
+    ensure_THROW(cc_->isWithinDistance(cc_.get(), 1), geos::util::UnsupportedOperationException);
+
+    // Valid / Simple
+    ensure_THROW(cc_->isSimple(), geos::util::UnsupportedOperationException);
+    ensure_THROW(cc_->isValid(), geos::util::UnsupportedOperationException);
+
+    // Operations
+    ensure_THROW(cc_->convexHull(), geos::util::UnsupportedOperationException);
+    ensure_THROW(cc_->buffer(1), geos::util::UnsupportedOperationException);
+    ensure_THROW(cc_->getCentroid(), geos::util::UnsupportedOperationException);
+    ensure_THROW(cc_->getBoundary(), geos::util::UnsupportedOperationException);
+
+    ensure("clone", cc_->equalsIdentical(cc_->clone().get()));
+
+    ensure("reverse", cc_->reverse()->equalsIdentical(wktreader_.read(""
+            "COMPOUNDCURVE ((2 2, 2 0), CIRCULARSTRING (2 0, 1 1, 0 0))").get()));
+    auto cc3 = cc_->reverse();
+    ensure_THROW(cc3->normalize(), geos::util::UnsupportedOperationException);
+}
+
+// GeometryFilter
+template<>
+template<>
+void object::test<4>()
+{
+    struct TestGeometryFilter : public geos::geom::GeometryFilter {
+        void filter_ro(const geos::geom::Geometry* g) override
+        {
+            calls++;
+            last_arg = g;
+        }
+
+        std::size_t calls = 0;
+        const Geometry* last_arg = nullptr;
+    };
+
+    TestGeometryFilter tgf;
+    cc_->apply_ro(&tgf);
+
+    ensure_equals(tgf.calls, 1u);
+    ensure_equals(tgf.last_arg, cc_.get());
+}
+
+// GeometryComponentFilter RO
+template<>
+template<>
+void object::test<5>()
+{
+    struct TestGeometryComponentFilter : public geos::geom::GeometryComponentFilter {
+        void filter_ro(const geos::geom::Geometry* g) override
+        {
+            calls++;
+            last_arg = g;
+        }
+
+        std::size_t calls = 0;
+        const Geometry* last_arg = nullptr;
+    };
+
+    TestGeometryComponentFilter tgf;
+    cc_->apply_ro(&tgf);
+
+    ensure_equals(tgf.calls, 1u);
+    ensure_equals(tgf.last_arg, cc_.get());
+}
+
+// CoordinateFilter RO
+template<>
+template<>
+void object::test<6>()
+{
+    struct TestCoordinateFilter : public geos::geom::CoordinateFilter {
+        void filter_ro(const geos::geom::Coordinate* x) override
+        {
+            coords.push_back(*x);
+        }
+
+        bool isDone() const override
+        {
+            return coords.size() >= 4;
+        }
+
+        std::vector<CoordinateXY> coords;
+    };
+
+    TestCoordinateFilter tcf;
+    cc_->apply_ro(&tcf);
+
+    ensure_equals(tcf.coords.size(), 4u);
+    ensure_equals(tcf.coords[0], CoordinateXY(0, 0));
+    ensure_equals(tcf.coords[1], CoordinateXY(1, 1));
+    ensure_equals(tcf.coords[2], CoordinateXY(2, 0));
+    ensure_equals(tcf.coords[3], CoordinateXY(2, 0));
+}
+
+// CoordinateFilter RW
+template<>
+template<>
+void object::test<7>()
+{
+    struct TestCoordinateFilter : public geos::geom::CoordinateFilter {
+        void filter_rw(geos::geom::Coordinate* c) const override
+        {
+            c->z = count;
+            count += 1.0;
+        }
+
+        bool isDone() const override
+        {
+            return count >= 4;
+        }
+
+        mutable double count = 0.0;
+    };
+
+    TestCoordinateFilter tcf;
+    cc_->apply_rw(&tcf);
+
+    ensure_equals(tcf.count, 4.0);
+    auto newCoords = cc_->getCoordinates();
+
+    ensure_equals(newCoords->getOrdinate(0, CoordinateSequence::Z), 0.0);
+    ensure_equals(newCoords->getOrdinate(1, CoordinateSequence::Z), 1.0);
+    ensure_equals(newCoords->getOrdinate(2, CoordinateSequence::Z), 2.0);
+    ensure_equals(newCoords->getOrdinate(3, CoordinateSequence::Z), 3.0);
+    ensure_same(newCoords->getOrdinate(4, CoordinateSequence::Z), geos::DoubleNotANumber);
+}
+
+// CoordinateSequenceFilter RO
+template<>
+template<>
+void object::test<8>()
+{
+    struct TestCoordinateSequenceFilter : public geos::geom::CoordinateSequenceFilter {
+        void filter_ro(const CoordinateSequence& seq, std::size_t i) override
+        {
+            args.emplace_back(&seq, i);
+        }
+
+        bool isDone() const override
+        {
+            return args.size() >= 4;
+        }
+
+        bool isGeometryChanged() const override
+        {
+            return false;
+        }
+
+        std::vector<std::pair<const CoordinateSequence*, std::size_t>> args;
+    };
+
+    TestCoordinateSequenceFilter tcsf;
+    cc_->apply_ro(tcsf);
+
+    ensure_equals(tcsf.args.size(), 4u);
+
+    ensure_equals(tcsf.args[0].first, cc_->getCurveN(0)->getCoordinatesRO());
+    ensure_equals(tcsf.args[0].second, 0u);
+
+    ensure_equals(tcsf.args[1].first, cc_->getCurveN(0)->getCoordinatesRO());
+    ensure_equals(tcsf.args[1].second, 1u);
+
+    ensure_equals(tcsf.args[2].first, cc_->getCurveN(0)->getCoordinatesRO());
+    ensure_equals(tcsf.args[2].second, 2u);
+
+    ensure_equals(tcsf.args[3].first, cc_->getCurveN(1)->getCoordinatesRO());
+    ensure_equals(tcsf.args[3].second, 0u);
+}
+
+
+}
+
+
+
diff --git a/tests/unit/geom/CurvePolygonTest.cpp b/tests/unit/geom/CurvePolygonTest.cpp
new file mode 100644
index 000000000..135f2dec3
--- /dev/null
+++ b/tests/unit/geom/CurvePolygonTest.cpp
@@ -0,0 +1,178 @@
+#include <tut/tut.hpp>
+#include <tut/tut_macros.hpp>
+
+#include <geos/geom/GeometryFactory.h>
+#include <geos/geom/CircularString.h>
+#include <geos/geom/CompoundCurve.h>
+#include <geos/geom/CurvePolygon.h>
+#include <geos/geom/IntersectionMatrix.h>
+#include <geos/io/WKTReader.h>
+
+using geos::geom::CoordinateXY;
+
+namespace tut {
+
+// Common data used by tests
+struct test_curvepolygon_data {
+
+    geos::geom::GeometryFactory::Ptr factory_ = geos::geom::GeometryFactory::create();
+    geos::io::WKTReader wktreader_;
+
+    std::unique_ptr<geos::geom::CurvePolygon> cp_;
+
+    test_curvepolygon_data() {
+        std::vector<std::unique_ptr<geos::geom::Curve>> holes;
+
+        std::vector<std::unique_ptr<geos::geom::SimpleCurve>> shell_sections;
+        shell_sections.emplace_back(
+             factory_->createCircularString({
+                 CoordinateXY(0, 0),
+                 CoordinateXY(2, 0),
+                 CoordinateXY(2, 1),
+                 CoordinateXY(2, 3),
+                 CoordinateXY(4, 3)
+        }));
+        shell_sections.emplace_back(
+             factory_->createLineString({
+                 CoordinateXY(4, 3),
+                 CoordinateXY(4, 5),
+                 CoordinateXY(1, 4),
+                 CoordinateXY(0, 0)
+        }));
+
+        auto shell = factory_->createCompoundCurve(std::move(shell_sections));
+
+        holes.emplace_back(factory_->createCircularString({
+            CoordinateXY(1.7, 1),
+            CoordinateXY(1.4, 0.4),
+            CoordinateXY(1.6, 0.4),
+            CoordinateXY(1.6, 0.5),
+            CoordinateXY(1.7, 1)
+        }));
+
+        cp_ = factory_->createCurvePolygon(std::move(shell), std::move(holes));
+    }
+};
+
+typedef test_group<test_curvepolygon_data> group;
+typedef group::object object;
+
+group test_curvepolygon_group("geos::geom::CurvePolygon");
+
+template<>
+template<>
+void object::test<1>()
+{
+    auto cp = factory_->createCurvePolygon(false, false);
+
+    ensure("isEmpty", cp->isEmpty());
+    ensure_equals("getNumPoints", cp->getNumPoints(), 0u);
+    ensure("hasZ", !cp->hasZ());
+    ensure("hasM", !cp->hasM());
+    ensure_equals("getCoordinateDimension", cp->getCoordinateDimension(), 2u);
+
+    ensure("getCoordinates", cp->getCoordinates()->isEmpty());
+    ensure("getCoordinate", cp->getCoordinate() == nullptr);
+
+    ensure_THROW(cp->getArea(), geos::util::UnsupportedOperationException);
+    ensure_equals("getLength", cp->getLength(), 0.0);
+}
+
+// Basic Geometry API
+template<>
+template<>
+void object::test<2>()
+{
+    // Geometry type functions
+    ensure_equals("getGeometryType", cp_->getGeometryType(), "CurvePolygon");
+    ensure_equals("getGeometryTypdId", cp_->getGeometryTypeId(), geos::geom::GEOS_CURVEPOLYGON);
+    ensure("isCollection", !cp_->isCollection());
+
+    // Geometry size functions
+    ensure("isEmpty", !cp_->isEmpty());
+    ensure_THROW(cp_->getArea(), geos::util::UnsupportedOperationException);
+    ensure_THROW(cp_->getLength(), geos::util::UnsupportedOperationException);
+    ensure_equals("getNumGeometries", cp_->getNumGeometries(), 1u);
+    ensure_equals("getNumPoints", cp_->getNumPoints(), 14u);
+    ensure_equals("getNumInteriorRing", cp_->getNumInteriorRing(), 1u);
+    ensure(!cp_->getEnvelopeInternal()->isNull());
+
+    // Geometry dimension functions
+    ensure_equals("getDimension", cp_->getDimension(), geos::geom::Dimension::A);
+    ensure("isLineal", !cp_->isLineal());
+    ensure("isPuntal", !cp_->isPuntal());
+    ensure("isPolygonal", cp_->isPolygonal());
+    ensure("hasDimension(L)", !cp_->hasDimension(geos::geom::Dimension::L));
+    ensure("hasDimension(P)", !cp_->hasDimension(geos::geom::Dimension::P));
+    ensure("hasDimension(A)", cp_->hasDimension(geos::geom::Dimension::A));
+    ensure("isDimensionStrict", cp_->isDimensionStrict(geos::geom::Dimension::A));
+    ensure("isMixedDimension", !cp_->isMixedDimension());
+    ensure_equals("getBoundaryDimension", cp_->getBoundaryDimension(), geos::geom::Dimension::L);
+
+    // Coordinate dimension functions
+    ensure("hasZ", !cp_->hasZ());
+    ensure("hasM", !cp_->hasM());
+    ensure_equals("getCoordinateDimension", cp_->getCoordinateDimension(), 2u);
+
+    // Coordinate access functions
+    ensure("getCoordinates", cp_->getCoordinates()->getSize() == 14u);
+    ensure_equals("getCoordinate", *cp_->getCoordinate(), CoordinateXY(0, 0));
+}
+
+// Operations
+template<>
+template<>
+void object::test<3>()
+{
+    // Predicates
+    ensure_THROW(cp_->contains(cp_.get()), geos::util::UnsupportedOperationException);
+    ensure_THROW(cp_->coveredBy(cp_.get()), geos::util::UnsupportedOperationException);
+    ensure_THROW(cp_->covers(cp_.get()), geos::util::UnsupportedOperationException);
+    ensure_THROW(cp_->crosses(cp_.get()), geos::util::UnsupportedOperationException);
+    ensure_THROW(cp_->disjoint(cp_.get()), geos::util::UnsupportedOperationException);
+    ensure_THROW(cp_->equals(cp_.get()), geos::util::UnsupportedOperationException);
+    ensure_THROW(cp_->intersects(cp_.get()), geos::util::UnsupportedOperationException);
+    ensure_THROW(cp_->overlaps(cp_.get()), geos::util::UnsupportedOperationException);
+    ensure_THROW(cp_->relate(cp_.get()), geos::util::UnsupportedOperationException);
+    ensure_THROW(cp_->touches(cp_.get()), geos::util::UnsupportedOperationException);
+    ensure_THROW(cp_->within(cp_.get()), geos::util::UnsupportedOperationException);
+
+    auto cp2 = cp_->clone();
+
+    ensure("equalsExact", cp_->equalsExact(cp2.get()));
+    ensure("equalsIdentical", cp_->equalsIdentical(cp2.get()));
+
+    // Overlay
+    ensure_THROW(cp_->Union(), geos::util::UnsupportedOperationException);
+    ensure_THROW(cp_->Union(cp_.get()), geos::util::UnsupportedOperationException);
+    ensure_THROW(cp_->difference(cp_.get()), geos::util::UnsupportedOperationException);
+    ensure_THROW(cp_->intersection(cp_.get()), geos::util::UnsupportedOperationException);
+    ensure_THROW(cp_->symDifference(cp_.get()), geos::util::UnsupportedOperationException);
+
+    // Distance
+    ensure_THROW(cp_->distance(cp_.get()), geos::util::UnsupportedOperationException);
+    ensure_THROW(cp_->isWithinDistance(cp_.get(), 1), geos::util::UnsupportedOperationException);
+
+    // Valid / Simple
+    ensure_THROW(cp_->isSimple(), geos::util::UnsupportedOperationException);
+    ensure_THROW(cp_->isValid(), geos::util::UnsupportedOperationException);
+
+    // Operations
+    ensure_THROW(cp_->convexHull(), geos::util::UnsupportedOperationException);
+    ensure_THROW(cp_->buffer(1), geos::util::UnsupportedOperationException);
+    ensure_THROW(cp_->getCentroid(), geos::util::UnsupportedOperationException);
+    ensure_THROW(cp_->getBoundary(), geos::util::UnsupportedOperationException);
+
+    ensure("clone", cp_->equalsIdentical(cp_->clone().get()));
+
+    // each element is reversed but the order of the elements remains the same
+    // this behavior is the same as for MultiLineString
+    ensure("reverse", cp_->reverse()->equalsIdentical(wktreader_.read(
+            "CURVEPOLYGON ("
+            "COMPOUNDCURVE ((0 0, 1 4, 4 5, 4 3), CIRCULARSTRING (4 3, 2 3, 2 1, 2 0, 0 0)), "
+            "CIRCULARSTRING (1.7 1, 1.6 0.5, 1.6 0.4, 1.4 0.4, 1.7 1))").get()));
+    auto cc3 = cp_->reverse();
+    ensure_THROW(cc3->normalize(), geos::util::UnsupportedOperationException);
+}
+
+}
diff --git a/tests/unit/geom/MultiCurveTest.cpp b/tests/unit/geom/MultiCurveTest.cpp
new file mode 100644
index 000000000..1fec0ebc2
--- /dev/null
+++ b/tests/unit/geom/MultiCurveTest.cpp
@@ -0,0 +1,199 @@
+#include <tut/tut.hpp>
+#include <tut/tut_macros.hpp>
+
+#include <geos/geom/GeometryFactory.h>
+#include <geos/geom/CompoundCurve.h>
+#include <geos/geom/CircularString.h>
+#include <geos/geom/IntersectionMatrix.h>
+#include <geos/geom/MultiCurve.h>
+#include <geos/io/WKTReader.h>
+
+using geos::geom::CoordinateXY;
+
+namespace tut {
+
+// Common data used by tests
+struct test_multicurve_data {
+
+    geos::geom::GeometryFactory::Ptr factory_ = geos::geom::GeometryFactory::create();
+    geos::io::WKTReader wktreader_;
+
+    std::unique_ptr<geos::geom::MultiCurve> mc_;
+
+    test_multicurve_data() {
+        std::vector<std::unique_ptr<geos::geom::Curve>> curves;
+
+        // Add a CompoundCurve
+        std::vector<std::unique_ptr<geos::geom::SimpleCurve>> cc_sections;
+        cc_sections.emplace_back(
+             factory_->createCircularString({
+                 CoordinateXY(0, 0),
+                 CoordinateXY(2, 0),
+                 CoordinateXY(2, 1),
+                 CoordinateXY(2, 3),
+                 CoordinateXY(4, 3)
+        }));
+        cc_sections.emplace_back(
+             factory_->createLineString({
+                 CoordinateXY(4, 3),
+                 CoordinateXY(4, 5),
+                 CoordinateXY(1, 4),
+                 CoordinateXY(0, 0)
+        }));
+
+        curves.emplace_back(factory_->createCompoundCurve(std::move(cc_sections)));
+
+        // Add a LineString
+        curves.emplace_back(factory_->createLineString({CoordinateXY(8, 9), CoordinateXY(10, 11)}));
+
+        // Add a CircularString
+        curves.emplace_back(factory_->createCircularString({
+            CoordinateXY(1.7, 1),
+            CoordinateXY(1.4, 0.4),
+            CoordinateXY(1.6, 0.4),
+            CoordinateXY(1.6, 0.5),
+            CoordinateXY(1.7, 1)
+        }));
+
+        mc_ = factory_->createMultiCurve(std::move(curves));
+    }
+
+};
+
+typedef test_group<test_multicurve_data> group;
+typedef group::object object;
+
+group test_multicurve_group("geos::geom::MultiCurve");
+
+template<>
+template<>
+void object::test<1>()
+{
+    auto mc = factory_->createMultiCurve();
+
+    ensure("isEmpty", mc->isEmpty());
+    ensure_equals("getNumPoints", mc->getNumPoints(), 0u);
+    ensure("hasZ", !mc->hasZ());
+    ensure("hasM", !mc->hasM());
+    ensure_equals("getCoordinateDimension", mc->getCoordinateDimension(), 2u);
+
+    ensure("getCoordinates", mc->getCoordinates()->isEmpty());
+    ensure("getCoordinate", mc->getCoordinate() == nullptr);
+
+    ensure_equals("getArea", mc->getArea(), 0);
+    ensure_THROW(mc_->getLength(), geos::util::UnsupportedOperationException);
+}
+
+// Basic Geometry API
+template<>
+template<>
+void object::test<2>()
+{
+    // Geometry type functions
+    ensure_equals("getGeometryType", mc_->getGeometryType(), "MultiCurve");
+    ensure_equals("getGeometryTypdId", mc_->getGeometryTypeId(), geos::geom::GEOS_MULTICURVE);
+    ensure("isCollection", !mc_->isCollection());
+
+    // Geometry size functions
+    ensure("isEmpty", !mc_->isEmpty());
+    ensure_equals("getArea", mc_->getArea(), 0);
+    ensure_THROW(mc_->getLength(), geos::util::UnsupportedOperationException);
+    ensure_equals("getNumGeometries", mc_->getNumGeometries(), 3u);
+    ensure_equals("getNumPoints", mc_->getNumPoints(), 16u);
+    ensure(!mc_->getEnvelopeInternal()->isNull());
+
+    // Geometry dimension functions
+    ensure_equals("getDimension", mc_->getDimension(), geos::geom::Dimension::L);
+    ensure("isLineal", mc_->isLineal());
+    ensure("isPuntal", !mc_->isPuntal());
+    ensure("isPolygonal", !mc_->isPolygonal());
+    ensure("hasDimension(L)", mc_->hasDimension(geos::geom::Dimension::L));
+    ensure("hasDimension(P)", !mc_->hasDimension(geos::geom::Dimension::P));
+    ensure("hasDimension(A)", !mc_->hasDimension(geos::geom::Dimension::A));
+    ensure("isDimensionStrict", mc_->isDimensionStrict(geos::geom::Dimension::L));
+    ensure("isMixedDimension", !mc_->isMixedDimension());
+    ensure_equals("getBoundaryDimension", mc_->getBoundaryDimension(), geos::geom::Dimension::P);
+
+    // Coordinate dimension functions
+    ensure("hasZ", !mc_->hasZ());
+    ensure("hasM", !mc_->hasM());
+    ensure_equals("getCoordinateDimension", mc_->getCoordinateDimension(), 2u);
+
+    // Coordinate access functions
+    ensure("getCoordinates", mc_->getCoordinates()->getSize() == 16u);
+    ensure_equals("getCoordinate", *mc_->getCoordinate(), CoordinateXY(0, 0));
+}
+
+// Operations
+template<>
+template<>
+void object::test<3>()
+{
+    // Predicates
+    ensure_THROW(mc_->contains(mc_.get()), geos::util::UnsupportedOperationException);
+    ensure_THROW(mc_->coveredBy(mc_.get()), geos::util::UnsupportedOperationException);
+    ensure_THROW(mc_->covers(mc_.get()), geos::util::UnsupportedOperationException);
+    ensure_THROW(mc_->crosses(mc_.get()), geos::util::UnsupportedOperationException);
+    ensure_THROW(mc_->disjoint(mc_.get()), geos::util::UnsupportedOperationException);
+    ensure_THROW(mc_->equals(mc_.get()), geos::util::UnsupportedOperationException);
+    ensure_THROW(mc_->intersects(mc_.get()), geos::util::UnsupportedOperationException);
+    ensure_THROW(mc_->overlaps(mc_.get()), geos::util::UnsupportedOperationException);
+    ensure_THROW(mc_->relate(mc_.get()), geos::util::UnsupportedOperationException);
+    ensure_THROW(mc_->touches(mc_.get()), geos::util::UnsupportedOperationException);
+    ensure_THROW(mc_->within(mc_.get()), geos::util::UnsupportedOperationException);
+
+    auto cc2 = mc_->clone();
+
+    ensure("equalsExact", mc_->equalsExact(cc2.get()));
+    ensure("equalsIdentical", mc_->equalsIdentical(cc2.get()));
+
+    // Overlay
+    ensure_THROW(mc_->Union(), geos::util::UnsupportedOperationException);
+    ensure_THROW(mc_->Union(mc_.get()), geos::util::UnsupportedOperationException);
+    ensure_THROW(mc_->difference(mc_.get()), geos::util::UnsupportedOperationException);
+    ensure_THROW(mc_->intersection(mc_.get()), geos::util::UnsupportedOperationException);
+    ensure_THROW(mc_->symDifference(mc_.get()), geos::util::UnsupportedOperationException);
+
+    // Distance
+    ensure_THROW(mc_->distance(mc_.get()), geos::util::UnsupportedOperationException);
+    ensure_THROW(mc_->isWithinDistance(mc_.get(), 1), geos::util::UnsupportedOperationException);
+
+    // Valid / Simple
+    ensure_THROW(mc_->isSimple(), geos::util::UnsupportedOperationException);
+    ensure_THROW(mc_->isValid(), geos::util::UnsupportedOperationException);
+
+    // Operations
+    ensure_THROW(mc_->convexHull(), geos::util::UnsupportedOperationException);
+    ensure_THROW(mc_->buffer(1), geos::util::UnsupportedOperationException);
+    ensure_THROW(mc_->getCentroid(), geos::util::UnsupportedOperationException);
+    ensure_THROW(mc_->getBoundary(), geos::util::UnsupportedOperationException);
+
+    ensure("clone", mc_->equalsIdentical(mc_->clone().get()));
+
+    // each element is reversed but the order of the elements remains the same
+    // this behavior is the same as for MultiLineString
+    ensure("reverse", mc_->reverse()->equalsIdentical(wktreader_.read(""
+            "MULTICURVE ("
+            "  COMPOUNDCURVE ((0 0, 1 4, 4 5, 4 3), CIRCULARSTRING (4 3, 2 3, 2 1, 2 0, 0 0)), "
+            "  (10 11, 8 9),"
+            "  CIRCULARSTRING (1.7 1, 1.6 0.5, 1.6 0.4, 1.4 0.4, 1.7 1))").get()));
+    auto cc3 = mc_->reverse();
+    ensure_THROW(cc3->normalize(), geos::util::UnsupportedOperationException);
+}
+
+// isClosed
+template<>
+template<>
+void object::test<4>()
+{
+    // union of elements is closed, but individual elements are not => MultiCurve is not closed
+    ensure(!wktreader_.read<geos::geom::MultiCurve>("MULTICURVE ((0 0, 1 0), (1 0, 1 1, 0 0))")->isClosed());
+
+    // all elements are closed => MulitCurve is closed
+    ensure(wktreader_.read<geos::geom::MultiCurve>("MULTICURVE ((0 0, 1 0, 1 1, 0 0), CIRCULARSTRING (3 3, 5 5, 3 3))")->isClosed());
+
+    // some elements are closed => MultiCurve is not closed
+    ensure(!wktreader_.read<geos::geom::MultiCurve>("MULTICURVE ((0 0, 1 0, 1 1, 0 0), CIRCULARSTRING (3 3, 4 4, 5 3))")->isClosed());
+}
+
+}
diff --git a/tests/unit/geom/MultiSurfaceTest.cpp b/tests/unit/geom/MultiSurfaceTest.cpp
new file mode 100644
index 000000000..08c51f9e5
--- /dev/null
+++ b/tests/unit/geom/MultiSurfaceTest.cpp
@@ -0,0 +1,170 @@
+#include <tut/tut.hpp>
+#include <tut/tut_macros.hpp>
+
+#include <geos/geom/CircularString.h>
+#include <geos/geom/CurvePolygon.h>
+#include <geos/geom/GeometryFactory.h>
+#include <geos/geom/MultiSurface.h>
+#include <geos/geom/IntersectionMatrix.h>
+#include <geos/io/WKTReader.h>
+
+using geos::geom::CoordinateXY;
+
+namespace tut {
+
+// Common data used by tests
+struct test_multisurface_data {
+
+    geos::geom::GeometryFactory::Ptr factory_ = geos::geom::GeometryFactory::create();
+    geos::io::WKTReader wktreader_;
+
+    std::unique_ptr<geos::geom::MultiSurface> ms_;
+
+    test_multisurface_data() {
+        std::vector<std::unique_ptr<geos::geom::Surface>> surfaces;
+
+        surfaces.emplace_back(
+            factory_->createPolygon(
+                factory_->createLinearRing({
+                     CoordinateXY(0, 0),
+                     CoordinateXY(1, 0),
+                     CoordinateXY(1, 1),
+                     CoordinateXY(0, 1),
+                     CoordinateXY(0, 0)
+        })));
+
+        surfaces.emplace_back(
+            factory_->createCurvePolygon(
+                factory_->createCircularString({
+                     CoordinateXY(10, 10),
+                     CoordinateXY(11, 11),
+                     CoordinateXY(12, 10),
+                     CoordinateXY(11, 9),
+                     CoordinateXY(10, 10)
+        })));
+
+        ms_ = factory_->createMultiSurface(std::move(surfaces));
+    }
+
+};
+
+typedef test_group<test_multisurface_data> group;
+typedef group::object object;
+
+group test_multisurface_group("geos::geom::MultiSurface");
+
+template<>
+template<>
+void object::test<1>()
+{
+    auto ms = factory_->createMultiSurface();
+
+    ensure("isEmpty", ms->isEmpty());
+    ensure_equals("getNumPoints", ms->getNumPoints(), 0u);
+    ensure("hasZ", !ms->hasZ());
+    ensure("hasM", !ms->hasM());
+    ensure_equals("getCoordinateDimension", ms->getCoordinateDimension(), 2u);
+
+    ensure("getCoordinates", ms->getCoordinates()->isEmpty());
+    ensure("getCoordinate", ms->getCoordinate() == nullptr);
+
+    ensure_equals("getArea", ms->getArea(), 0);
+    ensure_equals("getLength", ms->getLength(), 0.0);
+}
+
+// Basic Geometry API
+template<>
+template<>
+void object::test<2>()
+{
+    // Geometry type functions
+    ensure_equals("getGeometryType", ms_->getGeometryType(), "MultiSurface");
+    ensure_equals("getGeometryTypdId", ms_->getGeometryTypeId(), geos::geom::GEOS_MULTISURFACE);
+    ensure("isCollection", !ms_->isCollection());
+
+    // Geometry size functions
+    ensure("isEmpty", !ms_->isEmpty());
+    ensure_THROW(ms_->getArea(), geos::util::UnsupportedOperationException);
+    ensure_THROW(ms_->getLength(), geos::util::UnsupportedOperationException);
+    ensure_equals("getNumGeometries", ms_->getNumGeometries(), 2u);
+    ensure_equals("getNumPoints", ms_->getNumPoints(), 10u);
+    ensure(!ms_->getEnvelopeInternal()->isNull());
+
+    // Geometry dimension functions
+    ensure_equals("getDimension", ms_->getDimension(), geos::geom::Dimension::A);
+    ensure("isLineal", !ms_->isLineal());
+    ensure("isPuntal", !ms_->isPuntal());
+    ensure("isPolygonal", ms_->isPolygonal());
+    ensure("hasDimension(L)", !ms_->hasDimension(geos::geom::Dimension::L));
+    ensure("hasDimension(P)", !ms_->hasDimension(geos::geom::Dimension::P));
+    ensure("hasDimension(A)", ms_->hasDimension(geos::geom::Dimension::A));
+    ensure("isDimensionStrict", ms_->isDimensionStrict(geos::geom::Dimension::A));
+    ensure("isMixedDimension", !ms_->isMixedDimension());
+    ensure_equals("getBoundaryDimension", ms_->getBoundaryDimension(), geos::geom::Dimension::L);
+
+    // Coordinate dimension functions
+    ensure("hasZ", !ms_->hasZ());
+    ensure("hasM", !ms_->hasM());
+    ensure_equals("getCoordinateDimension", ms_->getCoordinateDimension(), 2u);
+
+    // Coordinate access functions
+    ensure("getCoordinates", ms_->getCoordinates()->getSize() == 10u);
+    ensure_equals("getCoordinate", *ms_->getCoordinate(), CoordinateXY(0, 0));
+}
+
+// Operations
+template<>
+template<>
+void object::test<3>()
+{
+    // Predicates
+    ensure_THROW(ms_->contains(ms_.get()), geos::util::UnsupportedOperationException);
+    ensure_THROW(ms_->coveredBy(ms_.get()), geos::util::UnsupportedOperationException);
+    ensure_THROW(ms_->covers(ms_.get()), geos::util::UnsupportedOperationException);
+    ensure_THROW(ms_->crosses(ms_.get()), geos::util::UnsupportedOperationException);
+    ensure_THROW(ms_->disjoint(ms_.get()), geos::util::UnsupportedOperationException);
+    ensure_THROW(ms_->equals(ms_.get()), geos::util::UnsupportedOperationException);
+    ensure_THROW(ms_->intersects(ms_.get()), geos::util::UnsupportedOperationException);
+    ensure_THROW(ms_->overlaps(ms_.get()), geos::util::UnsupportedOperationException);
+    ensure_THROW(ms_->relate(ms_.get()), geos::util::UnsupportedOperationException);
+    ensure_THROW(ms_->touches(ms_.get()), geos::util::UnsupportedOperationException);
+    ensure_THROW(ms_->within(ms_.get()), geos::util::UnsupportedOperationException);
+
+    auto cp2 = ms_->clone();
+
+    ensure("equalsExact", ms_->equalsExact(cp2.get()));
+    ensure("equalsIdentical", ms_->equalsIdentical(cp2.get()));
+
+    // Overlay
+    ensure_THROW(ms_->Union(), geos::util::UnsupportedOperationException);
+    ensure_THROW(ms_->Union(ms_.get()), geos::util::UnsupportedOperationException);
+    ensure_THROW(ms_->difference(ms_.get()), geos::util::UnsupportedOperationException);
+    ensure_THROW(ms_->intersection(ms_.get()), geos::util::UnsupportedOperationException);
+    ensure_THROW(ms_->symDifference(ms_.get()), geos::util::UnsupportedOperationException);
+
+    // Distance
+    ensure_THROW(ms_->distance(ms_.get()), geos::util::UnsupportedOperationException);
+    ensure_THROW(ms_->isWithinDistance(ms_.get(), 1), geos::util::UnsupportedOperationException);
+
+    // Valid / Simple
+    ensure_THROW(ms_->isSimple(), geos::util::UnsupportedOperationException);
+    ensure_THROW(ms_->isValid(), geos::util::UnsupportedOperationException);
+
+    // Operations
+    ensure_THROW(ms_->convexHull(), geos::util::UnsupportedOperationException);
+    ensure_THROW(ms_->buffer(1), geos::util::UnsupportedOperationException);
+    ensure_THROW(ms_->getCentroid(), geos::util::UnsupportedOperationException);
+    ensure_THROW(ms_->getBoundary(), geos::util::UnsupportedOperationException);
+
+    ensure("clone", ms_->equalsIdentical(ms_->clone().get()));
+
+    // each element is reversed but the order of the elements remains the same
+    // this behavior is the same as for MultiLineString
+    ensure("reverse", ms_->reverse()->equalsIdentical(wktreader_.read(
+    "MULTISURFACE (((0 0, 0 1, 1 1, 1 0, 0 0)), "
+                  "CURVEPOLYGON (CIRCULARSTRING (10 10, 11 9, 12 10, 11 11, 10 10)))").get()));
+    auto cc3 = ms_->reverse();
+    ensure_THROW(cc3->normalize(), geos::util::UnsupportedOperationException);
+}
+
+}
diff --git a/tests/unit/io/WKTReaderTest.cpp b/tests/unit/io/WKTReaderTest.cpp
index 7057582e4..8873141f9 100644
--- a/tests/unit/io/WKTReaderTest.cpp
+++ b/tests/unit/io/WKTReaderTest.cpp
@@ -496,8 +496,6 @@ void object::test<26>
     ensure_equals(geom->getGeometryTypeId(), geos::geom::GEOS_COMPOUNDCURVE);
     ensure_equals(geom->getNumPoints(), 5u);
 
-    ensure_equals(geom->getNumGeometries(), 2u);
-
     // explicit form
     auto geom2 = wktreader.read("COMPOUNDCURVE (CIRCULARSTRING (0 0, 1 1, 1 0), LINESTRING (1 0, 0 1))");
     ensure(geom->equalsIdentical(geom2.get()));

commit ccedb1d89c7b3f94d272cda0b027513430cdd37e
Author: Daniel Baston <dbaston at gmail.com>
Date:   Sat Mar 2 19:49:39 2024 -0500

    WKTReader: Add tests for alternate curved geometry format

diff --git a/tests/unit/io/WKTReaderTest.cpp b/tests/unit/io/WKTReaderTest.cpp
index 55a247d52..7057582e4 100644
--- a/tests/unit/io/WKTReaderTest.cpp
+++ b/tests/unit/io/WKTReaderTest.cpp
@@ -497,6 +497,10 @@ void object::test<26>
     ensure_equals(geom->getNumPoints(), 5u);
 
     ensure_equals(geom->getNumGeometries(), 2u);
+
+    // explicit form
+    auto geom2 = wktreader.read("COMPOUNDCURVE (CIRCULARSTRING (0 0, 1 1, 1 0), LINESTRING (1 0, 0 1))");
+    ensure(geom->equalsIdentical(geom2.get()));
 }
 
 // Read a CurvePolygon whose components are simple curves
@@ -530,6 +534,10 @@ void object::test<29>
     auto geom = wktreader.read("MULTICURVE( (0 0, 5 5), CIRCULARSTRING(4 0, 4 4, 8 4))");
 
     ensure_equals(geom->getGeometryTypeId(), geos::geom::GEOS_MULTICURVE);
+
+    // explicit form
+    auto geom2 = wktreader.read("MULTICURVE( LINESTRING(0 0, 5 5), CIRCULARSTRING(4 0, 4 4, 8 4))");
+    ensure(geom->equalsIdentical(geom2.get()));
 }
 
 // Read a MultiCurve whose elements contain CompoundCurves
@@ -552,6 +560,10 @@ void object::test<31>
     auto geom = wktreader.read("MULTISURFACE( CURVEPOLYGON( CIRCULARSTRING( 0 0, 4 0, 4 4, 0 4, 0 0), (1 1, 3 3, 3 1, 1 1)), ((10 10, 14 12, 11 10, 10 10), (11 11, 11.5 11, 11 11.5, 11 11)))");
 
     ensure_equals(geom->getGeometryTypeId(), geos::geom::GEOS_MULTISURFACE);
+
+    // explicit form
+    auto geom2 = wktreader.read("MULTISURFACE( CURVEPOLYGON( CIRCULARSTRING( 0 0, 4 0, 4 4, 0 4, 0 0), (1 1, 3 3, 3 1, 1 1)), POLYGON ((10 10, 14 12, 11 10, 10 10), (11 11, 11.5 11, 11 11.5, 11 11)))");
+    ensure(geom->equalsIdentical(geom2.get()));
 }
 
 

commit 61152e4b065b6267882c1ee6a39f180187a92a27
Author: Daniel Baston <dbaston at gmail.com>
Date:   Sun Feb 18 21:51:45 2024 -0500

    WKTWriter: Support curved types

diff --git a/include/geos/geom/GeometryFactory.h b/include/geos/geom/GeometryFactory.h
index f5ae08cc4..e0da35330 100644
--- a/include/geos/geom/GeometryFactory.h
+++ b/include/geos/geom/GeometryFactory.h
@@ -147,6 +147,7 @@ public:
 
     /// Creates an EMPTY Point
     std::unique_ptr<Point> createPoint(std::size_t coordinateDimension = 2) const;
+    std::unique_ptr<Point> createPoint(bool hasZ, bool hasM) const;
 
     /// Creates a Point using the given Coordinate
     std::unique_ptr<Point> createPoint(const Coordinate& coordinate) const;
@@ -164,7 +165,7 @@ public:
     std::unique_ptr<GeometryCollection> createGeometryCollection() const;
 
     /// Construct the EMPTY Geometry
-    std::unique_ptr<Geometry> createEmptyGeometry() const;
+    std::unique_ptr<Geometry> createEmptyGeometry(GeometryTypeId type = GEOS_GEOMETRYCOLLECTION, bool hasZ=false, bool hasM=false) const;
 
     /// Construct a GeometryCollection taking ownership of given arguments
     template<typename T>
@@ -228,6 +229,7 @@ public:
 
     /// Construct an EMPTY LinearRing
     std::unique_ptr<LinearRing> createLinearRing(std::size_t coordinateDimension = 2) const;
+    std::unique_ptr<LinearRing> createLinearRing(bool hasZ, bool hasM) const;
 
     /// Construct a LinearRing taking ownership of given arguments
     std::unique_ptr<LinearRing> createLinearRing(
@@ -269,6 +271,7 @@ public:
 
     /// Construct an EMPTY Polygon
     std::unique_ptr<Polygon> createPolygon(std::size_t coordinateDimension = 2) const;
+    std::unique_ptr<Polygon> createPolygon(bool hasZ, bool hasM) const;
 
     /// Construct a Polygon taking ownership of given arguments
     std::unique_ptr<Polygon> createPolygon(std::unique_ptr<LinearRing> && shell) const;
@@ -284,6 +287,9 @@ public:
                            const std::vector<LinearRing*>& holes) const;
 
 
+    /// Construct an EMPTY CurvePolygon
+    std::unique_ptr<CurvePolygon> createCurvePolygon(bool hasZ, bool hasM) const;
+
     /// Construct a CurvePolygon taking ownership of given arguments
     std::unique_ptr<CurvePolygon> createCurvePolygon(std::unique_ptr<Curve>&& shell) const;
 
@@ -292,6 +298,7 @@ public:
 
     /// Construct an EMPTY LineString
     std::unique_ptr<LineString> createLineString(std::size_t coordinateDimension = 2) const;
+    std::unique_ptr<LineString> createLineString(bool hasZ, bool hasM) const;
 
     /// Copy a LineString
     std::unique_ptr<LineString> createLineString(const LineString& ls) const;
@@ -305,7 +312,7 @@ public:
         const CoordinateSequence& coordinates) const;
 
     /// Construct an EMPTY CircularString
-    std::unique_ptr<CircularString> createCircularString(bool hasZ = false, bool hasM = false) const;
+    std::unique_ptr<CircularString> createCircularString(bool hasZ, bool hasM) const;
 
     /// Copy a CircularString
     std::unique_ptr<CircularString> createCircularString(const CircularString& ls) const;
diff --git a/include/geos/io/WKTReader.h b/include/geos/io/WKTReader.h
index 6f9afe216..7294d64f5 100644
--- a/include/geos/io/WKTReader.h
+++ b/include/geos/io/WKTReader.h
@@ -123,7 +123,8 @@ protected:
     static std::string getNextCloserOrComma(io::StringTokenizer* tokenizer);
     static std::string getNextCloser(io::StringTokenizer* tokenizer);
     static std::string getNextWord(io::StringTokenizer* tokenizer);
-    std::unique_ptr<geom::Geometry> readGeometryTaggedText(io::StringTokenizer* tokenizer, OrdinateSet& ordinateFlags) const;
+    std::unique_ptr<geom::Geometry> readGeometryTaggedText(io::StringTokenizer* tokenizer, OrdinateSet& ordinateFlags, const geom::GeometryTypeId* emptyType = nullptr) const;
+
     std::unique_ptr<geom::Point> readPointText(io::StringTokenizer* tokenizer, OrdinateSet& ordinateFlags) const;
     std::unique_ptr<geom::LineString> readLineStringText(io::StringTokenizer* tokenizer, OrdinateSet& ordinateFlags) const;
     std::unique_ptr<geom::LinearRing> readLinearRingText(io::StringTokenizer* tokenizer, OrdinateSet& ordinateFlags) const;
@@ -132,14 +133,16 @@ protected:
     std::unique_ptr<geom::MultiLineString> readMultiLineStringText(io::StringTokenizer* tokenizer, OrdinateSet& ordinateFlags) const;
     std::unique_ptr<geom::MultiPolygon> readMultiPolygonText(io::StringTokenizer* tokenizer, OrdinateSet& ordinateFlags) const;
     std::unique_ptr<geom::GeometryCollection> readGeometryCollectionText(io::StringTokenizer* tokenizer, OrdinateSet& ordinateFlags) const;
-
     std::unique_ptr<geom::CircularString> readCircularStringText(io::StringTokenizer* tokenizer, OrdinateSet& ordinateFlags) const;
     std::unique_ptr<geom::CompoundCurve> readCompoundCurveText(io::StringTokenizer* tokenizer, OrdinateSet& ordinateFlags) const;
     std::unique_ptr<geom::CurvePolygon> readCurvePolygonText(io::StringTokenizer* tokenizer, OrdinateSet& ordinateFlags) const;
     std::unique_ptr<geom::MultiCurve> readMultiCurveText(io::StringTokenizer* tokenizer, OrdinateSet& ordinateFlags) const;
     std::unique_ptr<geom::MultiSurface> readMultiSurfaceText(io::StringTokenizer* tokenizer, OrdinateSet& ordinateFlags) const;
 
+    /// Read the contents of a LINEARRING, LINESTRING, CIRCULARSTRING, or COMPOUNDCURVE
     std::unique_ptr<geom::Curve> readCurveText(io::StringTokenizer* tokenizer, OrdinateSet& ordinateFlags) const;
+
+    /// Read the contents of a POLYGON or a CURVEPOLYGON
     std::unique_ptr<geom::Geometry> readSurfaceText(io::StringTokenizer* tokenizer, OrdinateSet& ordinateFlags) const;
 private:
     const geom::GeometryFactory* geometryFactory;
diff --git a/include/geos/io/WKTWriter.h b/include/geos/io/WKTWriter.h
index a37973a87..e58c01c9c 100644
--- a/include/geos/io/WKTWriter.h
+++ b/include/geos/io/WKTWriter.h
@@ -39,6 +39,8 @@ class Coordinate;
 class CoordinateXY;
 class CoordinateXYZM;
 class CoordinateSequence;
+class Curve;
+class CompoundCurve;
 class Geometry;
 class GeometryCollection;
 class Point;
@@ -49,6 +51,8 @@ class MultiPoint;
 class MultiLineString;
 class MultiPolygon;
 class PrecisionModel;
+class SimpleCurve;
+class Surface;
 }
 namespace io {
 class Writer;
@@ -208,23 +212,28 @@ protected:
             int level,
             Writer& writer) const;
 
+    void appendTag(
+            const geom::Geometry& geometry,
+            OrdinateSet outputOrdinates,
+            Writer& writer) const;
+
     void appendPointTaggedText(
         const geom::Point& point,
         OrdinateSet outputOrdinates,
         int level, Writer& writer) const;
 
-    void appendLineStringTaggedText(
-        const geom::LineString& lineString,
+    void appendSimpleCurveTaggedText(
+        const geom::SimpleCurve& lineString,
         OrdinateSet outputOrdinates,
         int level, Writer& writer) const;
 
-    void appendLinearRingTaggedText(
-        const geom::LinearRing& lineString,
+    void appendCompoundCurveTaggedText(
+        const geom::CompoundCurve& lineString,
         OrdinateSet outputOrdinates,
         int level, Writer& writer) const;
 
-    void appendPolygonTaggedText(
-        const geom::Polygon& polygon,
+    void appendSurfaceTaggedText(
+        const geom::Surface& polygon,
         OrdinateSet outputOrdinates,
         int level, Writer& writer) const;
 
@@ -233,13 +242,13 @@ protected:
         OrdinateSet outputOrdinates,
         int level, Writer& writer) const;
 
-    void appendMultiLineStringTaggedText(
-        const geom::MultiLineString& multiLineString,
+    void appendMultiCurveTaggedText(
+        const geom::GeometryCollection& multiCurve,
         OrdinateSet outputOrdinates,
         int level, Writer& writer) const;
 
-    void appendMultiPolygonTaggedText(
-        const geom::MultiPolygon& multiPolygon,
+    void appendMultiSurfaceTaggedText(
+        const geom::GeometryCollection& multiSurface,
         OrdinateSet outputOrdinates,
         int level, Writer& writer) const;
 
@@ -263,13 +272,25 @@ protected:
 
     std::string writeNumber(double d) const;
 
+    void appendCurveText(
+        const geom::Curve& lineString,
+        OrdinateSet outputOrdinates,
+        int level, bool doIndent, Writer& writer) const;
+
+    void appendSimpleCurveText(
+        const geom::SimpleCurve& lineString,
+        OrdinateSet outputOrdinates,
+        int level, bool doIndent, Writer& writer) const;
+
+#if 0
     void appendLineStringText(
         const geom::LineString& lineString,
         OrdinateSet outputOrdinates,
         int level, bool doIndent, Writer& writer) const;
+#endif
 
-    void appendPolygonText(
-        const geom::Polygon& polygon,
+    void appendSurfaceText(
+        const geom::Surface& polygon,
         OrdinateSet outputOrdinates,
         int level, bool indentFirst, Writer& writer) const;
 
@@ -278,13 +299,13 @@ protected:
         OrdinateSet outputOrdinates,
         int level, Writer& writer) const;
 
-    void appendMultiLineStringText(
-        const geom::MultiLineString& multiLineString,
+    void appendMultiCurveText(
+        const geom::GeometryCollection& multiCurve,
         OrdinateSet outputOrdinates,
         int level, bool indentFirst, Writer& writer) const;
 
-    void appendMultiPolygonText(
-        const geom::MultiPolygon& multiPolygon,
+    void appendMultiSurfaceText(
+        const geom::GeometryCollection& multiSurface,
         OrdinateSet outputOrdinates,
         int level, Writer& writer) const;
 
@@ -299,8 +320,6 @@ private:
         INDENT = 2
     };
 
-//	static const int INDENT = 2;
-
     bool isFormatted;
 
     int roundingPrecision;
@@ -309,8 +328,6 @@ private:
 
     bool removeEmptyDimensions = false;
 
-    int level;
-
     static constexpr int coordsPerLine = 10;
 
     uint8_t defaultOutputDimension;
diff --git a/include/geos/util/string.h b/include/geos/util/string.h
index 32375e0ca..374dfa03b 100644
--- a/include/geos/util/string.h
+++ b/include/geos/util/string.h
@@ -25,5 +25,7 @@ bool endsWith(const std::string & s, char suffix);
 bool startsWith(const std::string & s, const std::string & prefix);
 bool startsWith(const std::string & s, char prefix);
 
+void toUpper(std::string& s);
+
+}
 }
-}
\ No newline at end of file
diff --git a/src/geom/GeometryFactory.cpp b/src/geom/GeometryFactory.cpp
index a34e7ddd2..a143da299 100644
--- a/src/geom/GeometryFactory.cpp
+++ b/src/geom/GeometryFactory.cpp
@@ -225,6 +225,14 @@ GeometryFactory::createPoint(std::size_t coordinateDimension) const
     return std::unique_ptr<Point>(new Point(std::move(seq), this));
 }
 
+/*public*/
+std::unique_ptr<Point>
+GeometryFactory::createPoint(bool hasZ, bool hasM) const
+{
+    CoordinateSequence seq(0u, hasZ, hasM);
+    return std::unique_ptr<Point>(new Point(std::move(seq), this));
+}
+
 /*public*/
 std::unique_ptr<Point>
 GeometryFactory::createPoint(std::unique_ptr<CoordinateSequence>&& coords) const
@@ -334,9 +342,26 @@ GeometryFactory::createGeometryCollection() const
 
 /*public*/
 std::unique_ptr<Geometry>
-GeometryFactory::createEmptyGeometry() const
+GeometryFactory::createEmptyGeometry(GeometryTypeId type, bool hasZ, bool hasM) const
 {
-    return createGeometryCollection();
+    switch (type) {
+        case GEOS_POINT: return createPoint(hasZ, hasM);
+        case GEOS_LINESTRING: return createLineString(hasZ, hasM);
+        case GEOS_LINEARRING: return createLinearRing(hasZ, hasM);
+        case GEOS_POLYGON: return createPolygon(hasZ, hasM);
+        case GEOS_MULTIPOINT: return createMultiPoint();
+        case GEOS_MULTILINESTRING: return createMultiLineString();
+        case GEOS_MULTIPOLYGON: return createMultiPolygon();
+        case GEOS_GEOMETRYCOLLECTION: return createGeometryCollection();
+        case GEOS_CIRCULARSTRING: return createCircularString(hasZ, hasM);
+        case GEOS_COMPOUNDCURVE: return createCompoundCurve();
+        case GEOS_CURVEPOLYGON: return createCurvePolygon(hasZ, hasM);
+        case GEOS_MULTICURVE: return createMultiCurve();
+        case GEOS_MULTISURFACE: return createMultiSurface();
+        default:
+            throw geos::util::IllegalArgumentException("Unexpected GeometryTypeId");
+
+    }
 }
 
 /*public*/
@@ -417,6 +442,15 @@ GeometryFactory::createLinearRing(std::size_t coordinateDimension) const
     return std::unique_ptr<LinearRing>(new LinearRing(std::move(cs), *this));
 }
 
+/*public*/
+std::unique_ptr<LinearRing>
+GeometryFactory::createLinearRing(bool hasZ, bool hasM) const
+{
+    // Can't use make_unique with protected constructor
+    auto cs = detail::make_unique<CoordinateSequence>(0u, hasZ, hasM);
+    return std::unique_ptr<LinearRing>(new LinearRing(std::move(cs), *this));
+}
+
 std::unique_ptr<LinearRing>
 GeometryFactory::createLinearRing(CoordinateSequence::Ptr && newCoords) const
 {
@@ -487,6 +521,15 @@ GeometryFactory::createPolygon(std::size_t coordinateDimension) const
     return createPolygon(std::move(lr));
 }
 
+/*public*/
+std::unique_ptr<Polygon>
+GeometryFactory::createPolygon(bool hasZ, bool hasM) const
+{
+    auto cs = detail::make_unique<CoordinateSequence>(0u, hasZ, hasM);
+    auto lr = createLinearRing(std::move(cs));
+    return createPolygon(std::move(lr));
+}
+
 std::unique_ptr<Polygon>
 GeometryFactory::createPolygon(std::unique_ptr<LinearRing> && shell)
 const
@@ -531,12 +574,20 @@ const
     return new Polygon(std::move(newRing), std::move(newHoles), *this);
 }
 
+/* public */
+std::unique_ptr<CurvePolygon>
+GeometryFactory::createCurvePolygon(bool hasZ, bool hasM)
+const
+{
+    // Can't use make_unique with protected constructor
+    return std::unique_ptr<CurvePolygon>(new CurvePolygon(createLinearRing(hasZ, hasM), *this));
+}
+
 /* public */
 std::unique_ptr<CurvePolygon>
 GeometryFactory::createCurvePolygon(std::unique_ptr<Curve> && shell)
 const
 {
-    //auto shellCurve = std::unique_ptr<Curve>(detail::down_cast<Curve*>(shell.release()));
     // Can't use make_unique with protected constructor
     return std::unique_ptr<CurvePolygon>(new CurvePolygon(std::move(shell), *this));
 }
@@ -558,6 +609,14 @@ GeometryFactory::createLineString(std::size_t coordinateDimension) const
     return createLineString(std::move(cs));
 }
 
+/*public*/
+std::unique_ptr<LineString>
+GeometryFactory::createLineString(bool hasZ, bool hasM) const
+{
+    auto cs = detail::make_unique<CoordinateSequence>(0u, hasZ, hasM);
+    return createLineString(std::move(cs));
+}
+
 /*public*/
 std::unique_ptr<CircularString>
 GeometryFactory::createCircularString(bool hasZ, bool hasM) const
@@ -599,7 +658,7 @@ GeometryFactory::createCircularString(CoordinateSequence::Ptr && newCoords)
 const
 {
     if (!newCoords)
-        return createCircularString();
+        return createCircularString(false, false);
     // Can't use make_unique with protected constructor
     return std::unique_ptr<CircularString>(new CircularString(std::move(newCoords), *this));
 }
diff --git a/src/io/WKTReader.cpp b/src/io/WKTReader.cpp
index f72bedede..fc850d220 100644
--- a/src/io/WKTReader.cpp
+++ b/src/io/WKTReader.cpp
@@ -265,14 +265,19 @@ WKTReader::getNextWord(StringTokenizer* tokenizer)
 }
 
 std::unique_ptr<Geometry>
-WKTReader::readGeometryTaggedText(StringTokenizer* tokenizer, OrdinateSet& ordinateFlags) const
+WKTReader::readGeometryTaggedText(StringTokenizer* tokenizer, OrdinateSet& ordinateFlags, const GeometryTypeId* emptyType) const
 {
     std::string type = getNextWord(tokenizer);
 
     std::unique_ptr<Geometry> geom;
     OrdinateSet origFlags = ordinateFlags;
+
     OrdinateSet newFlags = OrdinateSet::createXY();
-    readOrdinateFlags(type, newFlags);
+    if (type == "EMPTY") {
+        newFlags = origFlags;
+    } else {
+        readOrdinateFlags(type, newFlags);
+    }
 
     if(isTypeName(type, "POINT")) {
         geom = readPointText(tokenizer, newFlags);
@@ -312,6 +317,8 @@ WKTReader::readGeometryTaggedText(StringTokenizer* tokenizer, OrdinateSet& ordin
     }
     else if(isTypeName(type, "GEOMETRYCOLLECTION")) {
         geom = readGeometryCollectionText(tokenizer, newFlags);
+    } else if (type == "EMPTY" && emptyType != nullptr) {
+        return geometryFactory->createEmptyGeometry(*emptyType, newFlags.hasZ(), newFlags.hasM());
     } else {
         throw ParseException("Unknown type", type);
     }
@@ -327,7 +334,7 @@ WKTReader::readGeometryTaggedText(StringTokenizer* tokenizer, OrdinateSet& ordin
 std::unique_ptr<Point>
 WKTReader::readPointText(StringTokenizer* tokenizer, OrdinateSet& ordinateFlags) const
 {
-    auto coords = getCoordinates(tokenizer, ordinateFlags);
+    auto&& coords = getCoordinates(tokenizer, ordinateFlags);
     return geometryFactory->createPoint(std::move(coords));
 }
 
@@ -363,7 +370,8 @@ WKTReader::readCurveText(StringTokenizer* tokenizer, OrdinateSet& ordinateFlags)
         return readLineStringText(tokenizer, ordinateFlags);
     }
 
-    auto component = readGeometryTaggedText(tokenizer, ordinateFlags);
+    GeometryTypeId defaultType = GEOS_LINESTRING;
+    auto component = readGeometryTaggedText(tokenizer, ordinateFlags, &defaultType);
     if (dynamic_cast<Curve*>(component.get())) {
         return std::unique_ptr<Curve>(static_cast<Curve*>(component.release()));
     }
@@ -379,8 +387,9 @@ WKTReader::readSurfaceText(StringTokenizer* tokenizer, OrdinateSet& ordinateFlag
         return readPolygonText(tokenizer, ordinateFlags);
     }
 
-    auto component = readGeometryTaggedText(tokenizer, ordinateFlags);
-    if (dynamic_cast<CurvePolygon*>(component.get())) {
+    GeometryTypeId defaultType = GEOS_POLYGON;
+    auto component = readGeometryTaggedText(tokenizer, ordinateFlags, &defaultType);
+    if (dynamic_cast<Surface*>(component.get())) {
         return component;
     }
 
diff --git a/src/io/WKTWriter.cpp b/src/io/WKTWriter.cpp
index 86c12cdf8..116db3178 100644
--- a/src/io/WKTWriter.cpp
+++ b/src/io/WKTWriter.cpp
@@ -22,8 +22,10 @@
 #include <geos/io/Writer.h>
 #include <geos/io/CLocalizer.h>
 #include <geos/io/CheckOrdinatesFilter.h>
+#include <geos/geom/CircularString.h>
 #include <geos/geom/Coordinate.h>
 #include <geos/geom/CoordinateSequenceFilter.h>
+#include <geos/geom/CompoundCurve.h>
 #include <geos/geom/Point.h>
 #include <geos/geom/LinearRing.h>
 #include <geos/geom/LineString.h>
@@ -31,9 +33,12 @@
 #include <geos/geom/MultiPoint.h>
 #include <geos/geom/MultiLineString.h>
 #include <geos/geom/MultiPolygon.h>
+#include <geos/geom/MultiCurve.h>
+#include <geos/geom/MultiSurface.h>
 #include <geos/geom/CoordinateSequence.h>
 #include <geos/geom/PrecisionModel.h>
 #include <geos/util.h>
+#include <geos/util/string.h>
 #include <geos/util/IllegalArgumentException.h>
 
 #include <ryu/ryu.h>
@@ -58,7 +63,6 @@ WKTWriter::WKTWriter():
     isFormatted(false),
     roundingPrecision(-1),
     trim(true),
-    level(0),
     defaultOutputDimension(4),
     old3D(false)
 {
@@ -208,7 +212,7 @@ WKTWriter::writeFormatted(const Geometry* geometry, bool p_isFormatted,
 void
 WKTWriter::appendGeometryTaggedText(const Geometry& geometry,
                                     OrdinateSet checkOrdinates,
-                                    int p_level,
+                                    int level,
                                     Writer& writer) const
 {
     OrdinateSet outputOrdinates = OrdinateSet::createXY();
@@ -235,24 +239,33 @@ WKTWriter::appendGeometryTaggedText(const Geometry& geometry,
         }
     }
 
-    indent(p_level, &writer);
+    indent(level, &writer);
     switch(geometry.getGeometryTypeId()) {
-        case GEOS_POINT:      appendPointTaggedText(static_cast<const Point&>(geometry), outputOrdinates, p_level, writer); break;
-        case GEOS_LINESTRING: appendLineStringTaggedText(static_cast<const LineString&>(geometry), outputOrdinates, p_level, writer); break;
-        case GEOS_LINEARRING: appendLinearRingTaggedText(static_cast<const LinearRing&>(geometry), outputOrdinates, p_level, writer); break;
-        case GEOS_POLYGON:    appendPolygonTaggedText(static_cast<const Polygon&>(geometry), outputOrdinates, p_level, writer); break;
-        case GEOS_MULTIPOINT: appendMultiPointTaggedText(static_cast<const MultiPoint&>(geometry), outputOrdinates, p_level, writer); break;
-        case GEOS_MULTILINESTRING:    appendMultiLineStringTaggedText(static_cast<const MultiLineString&>(geometry), outputOrdinates, p_level, writer); break;
-        case GEOS_MULTIPOLYGON:       appendMultiPolygonTaggedText(static_cast<const MultiPolygon&>(geometry), outputOrdinates, p_level, writer); break;
-        case GEOS_GEOMETRYCOLLECTION: appendGeometryCollectionTaggedText(static_cast<const GeometryCollection&>(geometry), outputOrdinates, p_level, writer); break;
-        case GEOS_CIRCULARSTRING:
+        case GEOS_POINT:      appendPointTaggedText(static_cast<const Point&>(geometry), outputOrdinates, level, writer); break;
+        case GEOS_LINESTRING:
+        case GEOS_LINEARRING:
+        case GEOS_CIRCULARSTRING: appendSimpleCurveTaggedText(static_cast<const SimpleCurve&>(geometry), outputOrdinates, level, writer); break;
+        case GEOS_COMPOUNDCURVE: appendCompoundCurveTaggedText(static_cast<const CompoundCurve&>(geometry), outputOrdinates, level, writer); break;
         case GEOS_CURVEPOLYGON:
+        case GEOS_POLYGON:    appendSurfaceTaggedText(static_cast<const Polygon&>(geometry), outputOrdinates, level, writer); break;
+        case GEOS_MULTIPOINT: appendMultiPointTaggedText(static_cast<const MultiPoint&>(geometry), outputOrdinates, level, writer); break;
         case GEOS_MULTICURVE:
+        case GEOS_MULTILINESTRING:    appendMultiCurveTaggedText(static_cast<const MultiLineString&>(geometry), outputOrdinates, level, writer); break;
         case GEOS_MULTISURFACE:
-        case GEOS_COMPOUNDCURVE: throw std::runtime_error("Not handled yet");
+        case GEOS_MULTIPOLYGON:       appendMultiSurfaceTaggedText(static_cast<const MultiPolygon&>(geometry), outputOrdinates, level, writer); break;
+        case GEOS_GEOMETRYCOLLECTION: appendGeometryCollectionTaggedText(static_cast<const GeometryCollection&>(geometry), outputOrdinates, level, writer); break;
     }
 }
 
+void WKTWriter::appendTag(const Geometry& geometry, OrdinateSet outputOrdinates, Writer& writer) const
+{
+    std::string type = geometry.getGeometryType();
+    util::toUpper(type);
+    writer.write(type);
+    writer.write(" ");
+    appendOrdinateText(outputOrdinates, writer);
+}
+
 /*protected*/
 void
 WKTWriter::appendOrdinateText(OrdinateSet outputOrdinates, Writer& writer) const
@@ -279,7 +292,7 @@ WKTWriter::appendOrdinateText(OrdinateSet outputOrdinates, Writer& writer) const
 }
 
 void
-WKTWriter::appendPointTaggedText(const Point& point, OrdinateSet outputOrdinates, int p_level,
+WKTWriter::appendPointTaggedText(const Point& point, OrdinateSet outputOrdinates, int level,
                                  Writer& writer) const
 {
     writer.write("POINT ");
@@ -289,73 +302,103 @@ WKTWriter::appendPointTaggedText(const Point& point, OrdinateSet outputOrdinates
     if (coord == nullptr) {
         writer.write("EMPTY");
     } else {
-        appendSequenceText(*point.getCoordinatesRO(), outputOrdinates, p_level, false, writer);
+        appendSequenceText(*point.getCoordinatesRO(), outputOrdinates, level, false, writer);
     }
 }
 
 void
-WKTWriter::appendLineStringTaggedText(const LineString& lineString, OrdinateSet outputOrdinates, int p_level,
-                                      Writer& writer) const
+WKTWriter::appendSimpleCurveTaggedText(const SimpleCurve& curve, OrdinateSet outputOrdinates, int level, Writer& writer) const
 {
-    writer.write("LINESTRING ");
-    appendOrdinateText(outputOrdinates, writer);
-    appendSequenceText(*lineString.getCoordinatesRO(), outputOrdinates, p_level, false, writer);
-}
-
-/**
- * Converts a `LinearRing` to \<LinearRing Tagged Text\>
- * format, then appends it to the writer.
- *
- * @param  linearRing  the `LinearRing` to process
- * @param  writer      the output writer to append to
- */
-void
-WKTWriter::appendLinearRingTaggedText(const LinearRing& linearRing, OrdinateSet outputOrdinates, int p_level, Writer& writer) const
-{
-    writer.write("LINEARRING ");
-    appendOrdinateText(outputOrdinates, writer);
-    appendSequenceText(*linearRing.getCoordinatesRO(), outputOrdinates, p_level, false, writer);
+    appendTag(curve, outputOrdinates, writer);
+    appendSequenceText(*curve.getCoordinatesRO(), outputOrdinates, level, false, writer);
 }
 
 void
-WKTWriter::appendPolygonTaggedText(const Polygon& polygon, OrdinateSet outputOrdinates, int p_level, Writer& writer) const
-{
-    writer.write("POLYGON ");
-    appendOrdinateText(outputOrdinates, writer);
-    appendPolygonText(polygon, outputOrdinates, p_level, false, writer);
+WKTWriter::appendCurveText(const Curve& curve, OrdinateSet outputOrdinates, int level, bool doIndent, Writer& writer) const {
+    if (doIndent) {
+        indent(level, &writer);
+    }
+
+    if (curve.getGeometryTypeId() == GEOS_COMPOUNDCURVE) {
+        appendCompoundCurveTaggedText(static_cast<const CompoundCurve&>(curve), outputOrdinates, level, writer);
+    } else {
+        appendSimpleCurveText(static_cast<const SimpleCurve&>(curve), outputOrdinates, level, false, writer);
+    }
 }
 
 void
-WKTWriter::appendMultiPointTaggedText(const MultiPoint& multipoint, OrdinateSet outputOrdinates, int p_level, Writer& writer) const
+WKTWriter::appendSimpleCurveText(const SimpleCurve& curve, OrdinateSet outputOrdinates, int level, bool doIndent, Writer& writer) const {
+    if (doIndent) {
+        indent(level, &writer);
+    }
+
+    if (curve.getGeometryTypeId() == GEOS_CIRCULARSTRING) {
+        appendSimpleCurveTaggedText(curve, outputOrdinates, level, writer);
+    } else {
+        appendSequenceText(*curve.getCoordinatesRO(), outputOrdinates, level, false, writer);
+    }
+}
+
+
+void
+WKTWriter::appendCompoundCurveTaggedText(const CompoundCurve& curve, OrdinateSet outputOrdinates, int level, Writer& writer) const
+{
+    writer.write("COMPOUNDCURVE ");
+    appendOrdinateText(outputOrdinates, writer);
+
+    if (curve.isEmpty()) {
+        writer.write("EMPTY");
+    } else {
+        writer.write("(");
+        bool indentFirst = false;
+        for (std::size_t i = 0; i < curve.getNumCurves(); i++) {
+            if (i > 0) {
+                writer.write(", ");
+                indentFirst = true;
+            }
+
+            appendSimpleCurveText(*curve.getCurveN(i), outputOrdinates, level + (i > 0), indentFirst, writer);
+        }
+        writer.write(")");
+    }
+}
+
+void
+WKTWriter::appendSurfaceTaggedText(const Surface& surface, OrdinateSet outputOrdinates, int level, Writer& writer) const
+{
+    appendTag(surface, outputOrdinates, writer);
+    appendSurfaceText(surface, outputOrdinates, level, false, writer);
+}
+
+void
+WKTWriter::appendMultiPointTaggedText(const MultiPoint& multipoint, OrdinateSet outputOrdinates, int level, Writer& writer) const
 {
     writer.write("MULTIPOINT ");
     appendOrdinateText(outputOrdinates, writer);
-    appendMultiPointText(multipoint, outputOrdinates, p_level, writer);
+    appendMultiPointText(multipoint, outputOrdinates, level, writer);
 }
 
 void
-WKTWriter::appendMultiLineStringTaggedText(const MultiLineString& multiLineString, OrdinateSet outputOrdinates, int p_level, Writer& writer) const
+WKTWriter::appendMultiCurveTaggedText(const GeometryCollection& multiCurve, OrdinateSet outputOrdinates, int level, Writer& writer) const
 {
-    writer.write("MULTILINESTRING ");
-    appendOrdinateText(outputOrdinates, writer);
-    appendMultiLineStringText(multiLineString, outputOrdinates, p_level, false, writer);
+    appendTag(multiCurve, outputOrdinates, writer);
+    appendMultiCurveText(multiCurve, outputOrdinates, level, false, writer);
 }
 
 void
-WKTWriter::appendMultiPolygonTaggedText(const MultiPolygon& multiPolygon, OrdinateSet outputOrdinates, int p_level, Writer& writer) const
+WKTWriter::appendMultiSurfaceTaggedText(const GeometryCollection& multiPolygon, OrdinateSet outputOrdinates, int level, Writer& writer) const
 {
-    writer.write("MULTIPOLYGON ");
-    appendOrdinateText(outputOrdinates, writer);
-    appendMultiPolygonText(multiPolygon, outputOrdinates, p_level, writer);
+    appendTag(multiPolygon, outputOrdinates, writer);
+    appendMultiSurfaceText(multiPolygon, outputOrdinates, level, writer);
 }
 
 void
-WKTWriter::appendGeometryCollectionTaggedText(const GeometryCollection& geometryCollection, OrdinateSet outputOrdinates, int p_level,
+WKTWriter::appendGeometryCollectionTaggedText(const GeometryCollection& geometryCollection, OrdinateSet outputOrdinates, int level,
         Writer& writer) const
 {
     writer.write("GEOMETRYCOLLECTION ");
     appendOrdinateText(outputOrdinates, writer);
-    appendGeometryCollectionText(geometryCollection, outputOrdinates, p_level, writer);
+    appendGeometryCollectionText(geometryCollection, outputOrdinates, level, writer);
 }
 
 /* protected */
@@ -382,7 +425,7 @@ WKTWriter::appendCoordinate(const CoordinateXYZM& coordinate,
 void
 WKTWriter::appendSequenceText(const CoordinateSequence& seq,
                               OrdinateSet outputOrdinates,
-                              int p_level,
+                              int level,
                               bool doIndent,
                               Writer& writer) const
 {
@@ -391,7 +434,7 @@ WKTWriter::appendSequenceText(const CoordinateSequence& seq,
     }
     else {
         if(doIndent) {
-            indent(p_level, &writer);
+            indent(level, &writer);
         }
         writer.write("(");
         CoordinateXYZM c;
@@ -399,7 +442,7 @@ WKTWriter::appendSequenceText(const CoordinateSequence& seq,
             if(i > 0) {
                 writer.write(", ");
                 if(coordsPerLine > 0 && i % coordsPerLine == 0) {
-                    indent(p_level + 2, &writer);
+                    indent(level + 2, &writer);
                 }
             }
             seq.getAt(i, c);
@@ -409,7 +452,7 @@ WKTWriter::appendSequenceText(const CoordinateSequence& seq,
     }
 }
 
-int 
+int
 WKTWriter::writeTrimmedNumber(double d, uint32_t precision, char* buf)
 {
     const auto da = std::fabs(d);
@@ -464,14 +507,7 @@ WKTWriter::writeNumber(double d) const
 }
 
 void
-WKTWriter::appendLineStringText(const LineString& lineString, OrdinateSet outputOrdinates, int p_level,
-                                bool doIndent, Writer& writer) const
-{
-    appendSequenceText(*lineString.getCoordinatesRO(), outputOrdinates, p_level, doIndent, writer);
-}
-
-void
-WKTWriter::appendPolygonText(const Polygon& polygon, OrdinateSet outputOrdinates, int /*level*/,
+WKTWriter::appendSurfaceText(const Surface& polygon, OrdinateSet outputOrdinates, int level,
                              bool indentFirst, Writer& writer) const
 {
     if(polygon.isEmpty()) {
@@ -482,11 +518,15 @@ WKTWriter::appendPolygonText(const Polygon& polygon, OrdinateSet outputOrdinates
             indent(level, &writer);
         }
         writer.write("(");
-        appendLineStringText(*polygon.getExteriorRing(), outputOrdinates, level, false, writer);
+
+        auto ring = polygon.getExteriorRing();
+        appendCurveText(*ring, outputOrdinates, level, false, writer);
+
         for(std::size_t i = 0, n = polygon.getNumInteriorRing(); i < n; ++i) {
             writer.write(", ");
-            const LineString* ls = polygon.getInteriorRingN(i);
-            appendLineStringText(*ls, outputOrdinates, level + 1, true, writer);
+
+            auto hole = polygon.getInteriorRingN(i);
+            appendCurveText(*hole, outputOrdinates, level + 1, true, writer);
         }
         writer.write(")");
     }
@@ -523,49 +563,55 @@ WKTWriter::appendMultiPointText(const MultiPoint& multiPoint, OrdinateSet output
 }
 
 void
-WKTWriter::appendMultiLineStringText(const MultiLineString& multiLineString, OrdinateSet outputOrdinates, int p_level, bool indentFirst,
+WKTWriter::appendMultiCurveText(const GeometryCollection& multiCurve, OrdinateSet outputOrdinates, int level, bool indentFirst,
                                      Writer& writer) const
 {
-    const std::size_t n = multiLineString.getNumGeometries();
+    const std::size_t n = multiCurve.getNumGeometries();
     if(n == 0) {
         writer.write("EMPTY");
     }
     else {
-        int level2 = p_level;
+        int level2 = level;
         bool doIndent = indentFirst;
         writer.write("(");
         for(std::size_t i = 0; i < n; ++i) {
             if(i > 0) {
                 writer.write(", ");
-                level2 = p_level + 1;
+                level2 = level + 1;
                 doIndent = true;
             }
-            const LineString* ls = multiLineString.getGeometryN(i);
-            appendLineStringText(*ls, outputOrdinates, level2, doIndent, writer);
+
+            const Curve* g = static_cast<const Curve*>(multiCurve.getGeometryN(i));
+            appendCurveText(*g, outputOrdinates, level2, doIndent, writer);
         }
         writer.write(")");
     }
 }
 
 void
-WKTWriter::appendMultiPolygonText(const MultiPolygon& multiPolygon, OrdinateSet outputOrdinates, int p_level, Writer& writer) const
+WKTWriter::appendMultiSurfaceText(const GeometryCollection& multiSurface, OrdinateSet outputOrdinates, int level, Writer& writer) const
 {
-    const std::size_t n = multiPolygon.getNumGeometries();
+    const std::size_t n = multiSurface.getNumGeometries();
     if(n == 0) {
         writer.write("EMPTY");
     }
     else {
-        int level2 = p_level;
+        int level2 = level;
         bool doIndent = false;
         writer.write("(");
         for(std::size_t i = 0; i < n; ++i) {
             if(i > 0) {
                 writer.write(", ");
-                level2 = p_level + 1;
+                level2 = level + 1;
                 doIndent = true;
             }
-            const Polygon* p = multiPolygon.getGeometryN(i);
-            appendPolygonText(*p, outputOrdinates, level2, doIndent, writer);
+            const Surface* p = static_cast<const Surface*>(multiSurface.getGeometryN(i));
+            if (p->getGeometryTypeId() == GEOS_POLYGON) {
+                appendSurfaceText(*p, outputOrdinates, level2, doIndent, writer);
+            } else {
+                // FIXME indent
+                appendSurfaceTaggedText(*p, outputOrdinates, level2, writer);
+            }
         }
         writer.write(")");
     }
@@ -575,7 +621,7 @@ void
 WKTWriter::appendGeometryCollectionText(
     const GeometryCollection& geometryCollection,
     OrdinateSet outputOrdinates,
-    int p_level,
+    int level,
     Writer& writer) const
 {
     const std::size_t n = geometryCollection.getNumGeometries();
@@ -583,12 +629,12 @@ WKTWriter::appendGeometryCollectionText(
         writer.write("EMPTY");
     }
     else {
-        int level2 = p_level;
+        int level2 = level;
         writer.write("(");
         for(std::size_t i = 0; i < n; ++i) {
             if(i > 0) {
                 writer.write(", ");
-                level2 = p_level + 1;
+                level2 = level + 1;
             }
             appendGeometryTaggedText(*geometryCollection.getGeometryN(i), outputOrdinates, level2, writer);
         }
@@ -597,13 +643,13 @@ WKTWriter::appendGeometryCollectionText(
 }
 
 void
-WKTWriter::indent(int p_level, Writer* writer) const
+WKTWriter::indent(int level, Writer* writer) const
 {
-    if(!isFormatted || p_level <= 0) {
+    if(!isFormatted || level <= 0) {
         return;
     }
     writer->write("\n");
-    writer->write(std::string(INDENT * static_cast<std::size_t>(p_level), ' '));
+    writer->write(std::string(INDENT * static_cast<std::size_t>(level), ' '));
 }
 
 } // namespace geos.io
diff --git a/src/util/string.cpp b/src/util/string.cpp
index b28575a15..b61b147c1 100644
--- a/src/util/string.cpp
+++ b/src/util/string.cpp
@@ -12,6 +12,7 @@
  *
  **********************************************************************/
 
+#include <algorithm>
 #include <geos/util/string.h>
 
 namespace geos {
@@ -53,6 +54,11 @@ bool startsWith(const std::string & s, char prefix) {
     return s[0] == prefix;
 }
 
+void toUpper(std::string& s)
+{
+    std::transform(s.begin(), s.end(), s.begin(), ::toupper);
+}
+
 
 }
-}
\ No newline at end of file
+}
diff --git a/tests/unit/io/WKTWriterTest.cpp b/tests/unit/io/WKTWriterTest.cpp
index 7dbb7379d..8618f8be0 100644
--- a/tests/unit/io/WKTWriterTest.cpp
+++ b/tests/unit/io/WKTWriterTest.cpp
@@ -6,6 +6,11 @@
 // geos
 #include <geos/io/WKTReader.h>
 #include <geos/io/WKTWriter.h>
+#include <geos/geom/CircularString.h>
+#include <geos/geom/CompoundCurve.h>
+#include <geos/geom/CurvePolygon.h>
+#include <geos/geom/MultiCurve.h>
+#include <geos/geom/MultiSurface.h>
 #include <geos/geom/PrecisionModel.h>
 #include <geos/geom/GeometryFactory.h>
 #include <geos/geom/Point.h>
@@ -416,7 +421,8 @@ void object::test<15>
     // https://github.com/libgeos/geos/issues/888
     std::vector<std::string> variants0{
         "MULTIPOINT EMPTY", "MULTILINESTRING EMPTY",
-        "MULTIPOLYGON EMPTY", "GEOMETRYCOLLECTION EMPTY"
+        "MULTIPOLYGON EMPTY", "GEOMETRYCOLLECTION EMPTY",
+        "MULTICURVE EMPTY", "MULTISURFACE EMPTY"
     };
     for (const auto& wkt : variants0) {
         const auto g = wktreader.read(wkt);
@@ -435,7 +441,15 @@ void object::test<15>
         "GEOMETRYCOLLECTION (MULTIPOINT EMPTY)",
         "GEOMETRYCOLLECTION Z (POINT Z EMPTY)",
         "GEOMETRYCOLLECTION M (LINESTRING M EMPTY)",
-        "GEOMETRYCOLLECTION ZM (POLYGON ZM EMPTY)"
+        "GEOMETRYCOLLECTION ZM (POLYGON ZM EMPTY)",
+        "MULTICURVE (EMPTY)", "MULTICURVE Z (EMPTY)",
+        "MULTICURVE M (EMPTY)", "MULTICURVE ZM (EMPTY)",
+        "MULTICURVE (CIRCULARSTRING EMPTY)", "MULTICURVE Z (CIRCULARSTRING Z EMPTY)",
+        "MULTICURVE M (CIRCULARSTRING M EMPTY)", "MULTICURVE ZM (CIRCULARSTRING ZM EMPTY)",
+        "MULTISURFACE (EMPTY)", "MULTISURFACE Z (EMPTY)",
+        "MULTISURFACE M (EMPTY)", "MULTISURFACE ZM (EMPTY)",
+        "MULTISURFACE (EMPTY)", "MULTISURFACE Z (CURVEPOLYGON Z EMPTY)",
+        "MULTISURFACE M (CURVEPOLYGON M EMPTY)", "MULTISURFACE ZM (CURVEPOLYGON ZM EMPTY)",
     };
     for (const auto& wkt : variants1) {
         const auto g = wktreader.read(wkt);
@@ -454,7 +468,11 @@ void object::test<15>
         "GEOMETRYCOLLECTION (POLYGON EMPTY, LINESTRING EMPTY)",
         "GEOMETRYCOLLECTION Z (LINESTRING Z EMPTY, POINT Z EMPTY)",
         "GEOMETRYCOLLECTION M (POINT M EMPTY, LINESTRING M EMPTY)",
-        "GEOMETRYCOLLECTION ZM (POINT ZM EMPTY, LINESTRING ZM EMPTY)"
+        "GEOMETRYCOLLECTION ZM (POINT ZM EMPTY, LINESTRING ZM EMPTY)",
+        "MULTICURVE (EMPTY, CIRCULARSTRING EMPTY)", "MULTICURVE Z (EMPTY, CIRCULARSTRING Z EMPTY)",
+        "MULTICURVE M (EMPTY, CIRCULARSTRING M EMPTY)", "MULTICURVE ZM (EMPTY, CIRCULARSTRING ZM EMPTY)",
+        "MULTISURFACE (EMPTY, EMPTY)", "MULTISURFACE Z (EMPTY, CURVEPOLYGON Z EMPTY)",
+        "MULTISURFACE M (EMPTY, CURVEPOLYGON M EMPTY)", "MULTISURFACE ZM (EMPTY, CURVEPOLYGON ZM EMPTY)",
     };
     for (const auto& wkt : variants2) {
         const auto g = wktreader.read(wkt);
@@ -575,4 +593,227 @@ void object::test<16>
 
 }
 
+// test CircularString
+template<>
+template<>
+void object::test<17>()
+{
+    CoordinateSequence seq{
+        CoordinateXY(0, 0),
+        CoordinateXY(1, 1),
+        CoordinateXY(2, 0)
+    };
+    auto geom = gf->createCircularString(std::move(seq));
+
+    ensure_equals(wktwriter.write(*geom), "CIRCULARSTRING (0 0, 1 1, 2 0)");
+}
+
+// test CompoundCurve
+template<>
+template<>
+void object::test<18>()
+{
+    std::vector<std::unique_ptr<geos::geom::SimpleCurve>> curves;
+
+    curves.emplace_back(gf->createCircularString({
+        CoordinateXY(0, 0),
+        CoordinateXY(1, 1),
+        CoordinateXY(2, 0)
+    }));
+
+    curves.emplace_back(gf->createLineString({
+         CoordinateXY(2, 0),
+         CoordinateXY(2, 2)
+     }));
+
+    auto geom = gf->createCompoundCurve(std::move(curves));
+
+    ensure_equals(wktwriter.write(*geom), "COMPOUNDCURVE (CIRCULARSTRING (0 0, 1 1, 2 0), (2 0, 2 2))");
+}
+
+// test CurvePolygon
+template<>
+template<>
+void object::test<19>()
+{
+    std::vector<std::unique_ptr<geos::geom::Curve>> holes;
+
+    std::vector<std::unique_ptr<geos::geom::SimpleCurve>> shell_sections;
+    shell_sections.emplace_back(
+         gf->createCircularString({
+             CoordinateXY(0, 0),
+             CoordinateXY(2, 0),
+             CoordinateXY(2, 1),
+             CoordinateXY(2, 3),
+             CoordinateXY(4, 3)
+    }));
+    shell_sections.emplace_back(
+         gf->createLineString({
+             CoordinateXY(4, 3),
+             CoordinateXY(4, 5),
+             CoordinateXY(1, 4),
+             CoordinateXY(0, 0)
+    }));
+
+    auto shell = gf->createCompoundCurve(std::move(shell_sections));
+
+    holes.emplace_back(gf->createCircularString({
+        CoordinateXY(1.7, 1),
+        CoordinateXY(1.4, 0.4),
+        CoordinateXY(1.6, 0.4),
+        CoordinateXY(1.6, 0.5),
+        CoordinateXY(1.7, 1)
+    }));
+
+    auto geom = gf->createCurvePolygon(std::move(shell), std::move(holes));
+
+    ensure_equals(wktwriter.write(*geom), "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))");
+}
+
+// test MultiCurve
+template<>
+template<>
+void object::test<20>()
+{
+    std::vector<std::unique_ptr<geos::geom::Curve>> curves;
+
+    // Add a CompoundCurve
+    std::vector<std::unique_ptr<geos::geom::SimpleCurve>> cc_sections;
+    cc_sections.emplace_back(
+         gf->createCircularString({
+             CoordinateXY(0, 0),
+             CoordinateXY(2, 0),
+             CoordinateXY(2, 1),
+             CoordinateXY(2, 3),
+             CoordinateXY(4, 3)
+    }));
+    cc_sections.emplace_back(
+         gf->createLineString({
+             CoordinateXY(4, 3),
+             CoordinateXY(4, 5),
+             CoordinateXY(1, 4),
+             CoordinateXY(0, 0)
+    }));
+
+    curves.emplace_back(gf->createCompoundCurve(std::move(cc_sections)));
+
+    // Add a LineString
+    curves.emplace_back(gf->createLineString({CoordinateXY(8, 9), CoordinateXY(10, 11)}));
+
+    // Add a CircularString
+    curves.emplace_back(gf->createCircularString({
+        CoordinateXY(1.7, 1),
+        CoordinateXY(1.4, 0.4),
+        CoordinateXY(1.6, 0.4),
+        CoordinateXY(1.6, 0.5),
+        CoordinateXY(1.7, 1)
+    }));
+
+    auto geom = gf->createMultiCurve(std::move(curves));
+
+    ensure_equals(wktwriter.write(*geom), "MULTICURVE (COMPOUNDCURVE (CIRCULARSTRING (0 0, 2 0, 2 1, 2 3, 4 3), (4 3, 4 5, 1 4, 0 0)), (8 9, 10 11), CIRCULARSTRING (1.7 1, 1.4 0.4, 1.6 0.4, 1.6 0.5, 1.7 1))");
+}
+
+// test MultiSurface
+template<>
+template<>
+void object::test<21>()
+{
+    std::vector<std::unique_ptr<geos::geom::Surface>> surfaces;
+
+    surfaces.emplace_back(
+        gf->createPolygon(
+            gf->createLinearRing({
+                 CoordinateXY(0, 0),
+                 CoordinateXY(1, 0),
+                 CoordinateXY(1, 1),
+                 CoordinateXY(0, 1),
+                 CoordinateXY(0, 0)
+    })));
+
+    surfaces.emplace_back(
+        gf->createCurvePolygon(
+            gf->createCircularString({
+                 CoordinateXY(10, 10),
+                 CoordinateXY(11, 11),
+                 CoordinateXY(12, 10),
+                 CoordinateXY(11, 9),
+                 CoordinateXY(10, 10)
+    })));
+
+    auto geom = gf->createMultiSurface(std::move(surfaces));
+
+    ensure_equals(wktwriter.write(*geom), "MULTISURFACE (((0 0, 1 0, 1 1, 0 1, 0 0)), CURVEPOLYGON (CIRCULARSTRING (10 10, 11 11, 12 10, 11 9, 10 10)))");
+}
+
+// test formatted output
+template<>
+template<>
+void object::test<22>()
+{
+    auto geom = wktreader.read("POINT (1 1)");
+    ensure_equals(wktwriter.writeFormatted(geom.get()), "POINT (1 1)");
+
+    geom = wktreader.read("LINESTRING (1 2, 3 4)");
+    ensure_equals(wktwriter.writeFormatted(geom.get()), "LINESTRING (1 2, 3 4)");
+
+    geom = wktreader.read("LINEARRING (0 0, 1 0, 1 1, 0 0)");
+    ensure_equals(wktwriter.writeFormatted(geom.get()), "LINEARRING (0 0, 1 0, 1 1, 0 0)");
+
+    geom = wktreader.read("CIRCULARSTRING (0 0, 1 1, 2 0)");
+    ensure_equals(wktwriter.writeFormatted(geom.get()), "CIRCULARSTRING (0 0, 1 1, 2 0)");
+
+    geom = wktreader.read("COMPOUNDCURVE((0 10, 0 5), CIRCULARSTRING (0 0, 1 1, 2 0), (2 0, 3 0))");
+    ensure_equals(wktwriter.writeFormatted(geom.get()), "COMPOUNDCURVE ((0 10, 0 5), \n"
+                                                        "  CIRCULARSTRING (0 0, 1 1, 2 0), \n"
+                                                        "  (2 0, 3 0))");
+
+    geom = wktreader.read("POLYGON ((0 0, 10 0, 10 10, 0 10, 0 0), (1 1, 1 2, 2 2, 2 1, 1 1), (3 3, 3 4, 4 4, 4 3, 3 3))");
+    ensure_equals(wktwriter.writeFormatted(geom.get()), "POLYGON ((0 0, 10 0, 10 10, 0 10, 0 0), \n"
+                                                        "  (1 1, 1 2, 2 2, 2 1, 1 1), \n"
+                                                        "  (3 3, 3 4, 4 4, 4 3, 3 3))");
+
+    geom = wktreader.read("CURVEPOLYGON ((0 0, 10 0, 10 10, 0 10, 0 0), (1 1, 1 2, 2 2, 2 1, 1 1), CIRCULARSTRING (3 3, 3 4, 5 3, 3 2, 3 3))");
+    ensure_equals(wktwriter.writeFormatted(geom.get()), "CURVEPOLYGON ((0 0, 10 0, 10 10, 0 10, 0 0), \n"
+                                                        "  (1 1, 1 2, 2 2, 2 1, 1 1), \n"
+                                                        "  CIRCULARSTRING (3 3, 3 4, 5 3, 3 2, 3 3))");
+
+    geom = wktreader.read("MULTIPOINT ((0 0), (1 1), (2 2))");
+    ensure_equals(wktwriter.writeFormatted(geom.get()), "MULTIPOINT ((0 0), (1 1), (2 2))");
+
+    geom = wktreader.read("MULTILINESTRING ((0 0, 1 1), (2 2, 3 3), (4 4, 5 5))");
+    ensure_equals(wktwriter.writeFormatted(geom.get()), "MULTILINESTRING ((0 0, 1 1), \n"
+                                                        "  (2 2, 3 3), \n"
+                                                        "  (4 4, 5 5))");
+
+    geom = wktreader.read("MULTICURVE ((0 0, 1 1), COMPOUNDCURVE ((2 2, 3 3), CIRCULARSTRING (4 4, 5 5, 6 4), (6 4, 7 4)), (100 100, 200 200))");
+    ensure_equals(wktwriter.writeFormatted(geom.get()), "MULTICURVE ((0 0, 1 1), \n"
+                                                        "  COMPOUNDCURVE ((2 2, 3 3), \n"
+                                                        "    CIRCULARSTRING (4 4, 5 5, 6 4), \n"
+                                                        "    (6 4, 7 4)), \n"
+                                                        "  (100 100, 200 200))");
+
+    geom = wktreader.read("MULTIPOLYGON (((0 0, 10 0, 10 10, 0 10, 0 0), (1 1, 1 2, 2 2, 2 1, 1 1), (3 3, 3 4, 4 4, 4 3, 3 3)), ((100 100, 200 100, 200 200, 100 100)))");
+    ensure_equals(wktwriter.writeFormatted(geom.get()), "MULTIPOLYGON (((0 0, 10 0, 10 10, 0 10, 0 0), \n"
+                                                        "  (1 1, 1 2, 2 2, 2 1, 1 1), \n"
+                                                        "  (3 3, 3 4, 4 4, 4 3, 3 3)), \n"
+                                                        "  ((100 100, 200 100, 200 200, 100 100)))");
+
+    geom = wktreader.read("MULTISURFACE (CURVEPOLYGON ((0 0, 10 0, 10 10, 0 10, 0 0), (1 1, 1 2, 2 2, 2 1, 1 1), CIRCULARSTRING (3 3, 3 4, 5 3, 3 2, 3 3)), ((100 100, 200 100, 200 200, 100 100)))");
+    ensure_equals(wktwriter.writeFormatted(geom.get()), "MULTISURFACE (CURVEPOLYGON ((0 0, 10 0, 10 10, 0 10, 0 0), \n"
+                                                        "  (1 1, 1 2, 2 2, 2 1, 1 1), \n"
+                                                        "  CIRCULARSTRING (3 3, 3 4, 5 3, 3 2, 3 3)), \n"
+                                                        "  ((100 100, 200 100, 200 200, 100 100)))");
+
+    geom = wktreader.read("GEOMETRYCOLLECTION (LINESTRING (0 0, 1 1), MULTIPOLYGON (((0 0, 10 0, 10 10, 0 10, 0 0), (1 1, 1 2, 2 2, 2 1, 1 1), (3 3, 3 4, 4 4, 4 3, 3 3)), ((100 100, 200 100, 200 200, 100 100))), POINT (2 2))");
+    ensure_equals(wktwriter.writeFormatted(geom.get()), "GEOMETRYCOLLECTION (LINESTRING (0 0, 1 1), \n"
+                                                        "  MULTIPOLYGON (((0 0, 10 0, 10 10, 0 10, 0 0), \n"
+                                                        "    (1 1, 1 2, 2 2, 2 1, 1 1), \n"
+                                                        "    (3 3, 3 4, 4 4, 4 3, 3 3)), \n"
+                                                        "    ((100 100, 200 100, 200 200, 100 100))), \n"
+                                                        "  POINT (2 2))");
+}
+
+
+
 } // namespace tut

commit 365cb578445a6bd74a069cac8aa1705658a7772f
Author: Daniel Baston <dbaston at gmail.com>
Date:   Tue Jan 30 21:01:56 2024 -0500

    Add classes for curved geometry types

diff --git a/include/geos/geom/CircularString.h b/include/geos/geom/CircularString.h
new file mode 100644
index 000000000..3e7db7bbc
--- /dev/null
+++ b/include/geos/geom/CircularString.h
@@ -0,0 +1,54 @@
+/**********************************************************************
+ *
+ * GEOS - Geometry Engine Open Source
+ * http://geos.osgeo.org
+ *
+ * Copyright (C) 2024 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/geom/SimpleCurve.h>
+
+namespace geos {
+namespace geom {
+
+class GEOS_DLL CircularString : public SimpleCurve {
+
+public:
+    using SimpleCurve::SimpleCurve;
+
+    friend class GeometryFactory;
+
+    ~CircularString() override;
+
+    std::unique_ptr<CircularString> clone() const;
+
+    std::string getGeometryType() const override;
+
+    GeometryTypeId getGeometryTypeId() const override;
+
+    CircularString* cloneImpl() const override
+    {
+        return new CircularString(*this);
+    }
+
+    CircularString* reverseImpl() const override;
+
+    int
+    getSortIndex() const override
+    {
+        return SORTINDEX_LINESTRING;
+    };
+
+};
+
+
+}
+}
diff --git a/include/geos/geom/CompoundCurve.h b/include/geos/geom/CompoundCurve.h
new file mode 100644
index 000000000..e3a49804a
--- /dev/null
+++ b/include/geos/geom/CompoundCurve.h
@@ -0,0 +1,124 @@
+/**********************************************************************
+ *
+ * GEOS - Geometry Engine Open Source
+ * http://geos.osgeo.org
+ *
+ * Copyright (C) 2024 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/geom/SimpleCurve.h>
+#include <vector>
+
+namespace geos {
+namespace geom {
+
+class GEOS_DLL CompoundCurve : public Curve {
+    friend class GeometryFactory;
+
+
+public:
+
+    CompoundCurve(const CompoundCurve&);
+
+    CompoundCurve& operator=(const CompoundCurve&);
+
+    std::unique_ptr<CoordinateSequence> getCoordinates() const override;
+
+    const CoordinateXY* getCoordinate() const override;
+
+    uint8_t getCoordinateDimension() const override;
+
+    bool hasZ() const override;
+
+    bool hasM() const override;
+
+    bool isEmpty() const override;
+
+    bool isClosed() const override;
+
+    std::size_t getNumCurves() const;
+
+    const SimpleCurve* getCurveN(std::size_t) const;
+
+    std::size_t getNumGeometries() const override;
+
+    const Geometry* getGeometryN(std::size_t) const override;
+
+    std::size_t getNumPoints() const override;
+
+    std::unique_ptr<Geometry> getBoundary() const override;
+
+    std::string getGeometryType() const override;
+
+    GeometryTypeId getGeometryTypeId() const override;
+
+    bool equalsExact(const Geometry* other, double tolerance = 0)
+    const override;
+
+    bool equalsIdentical(const Geometry* other) const override;
+
+    const Envelope* getEnvelopeInternal() const override
+    {
+        return &envelope;
+    }
+
+    std::unique_ptr<CompoundCurve> clone() const;
+
+    CompoundCurve* cloneImpl() const override;
+
+    std::unique_ptr<CompoundCurve> reverse() const;
+
+    CompoundCurve* reverseImpl() const override;
+
+    void apply_rw(const CoordinateFilter* filter) override;
+
+    void apply_ro(CoordinateFilter* filter) const override;
+
+    void apply_rw(GeometryFilter* filter) override;
+
+    void apply_ro(GeometryFilter* filter) const override;
+
+    void apply_rw(GeometryComponentFilter* filter) override;
+
+    void apply_ro(GeometryComponentFilter* filter) const override;
+
+    void apply_rw(CoordinateSequenceFilter& filter) override;
+
+    void apply_ro(CoordinateSequenceFilter& filter) const override;
+
+    void normalize() override;
+
+    int compareToSameClass(const Geometry* geom) const override;
+
+
+protected:
+    CompoundCurve(std::vector<std::unique_ptr<SimpleCurve>>&&,
+                  const GeometryFactory&);
+
+    int getSortIndex() const override
+    {
+        return SORTINDEX_COMPOUNDCURVE;
+    }
+
+    void geometryChangedAction() override
+    {
+        envelope = computeEnvelopeInternal();
+    }
+
+    Envelope computeEnvelopeInternal();
+
+private:
+    std::vector<std::unique_ptr<SimpleCurve>> curves;
+    Envelope envelope;
+};
+
+}
+}
diff --git a/include/geos/geom/Curve.h b/include/geos/geom/Curve.h
new file mode 100644
index 000000000..10eea9580
--- /dev/null
+++ b/include/geos/geom/Curve.h
@@ -0,0 +1,46 @@
+/**********************************************************************
+ *
+ * GEOS - Geometry Engine Open Source
+ * http://geos.osgeo.org
+ *
+ * Copyright (C) 2024 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/geom/Geometry.h>
+
+namespace geos {
+namespace geom {
+
+class GEOS_DLL Curve : public Geometry {
+
+public:
+    /// Returns line dimension (1)
+    Dimension::DimensionType getDimension() const override
+    {
+        return Dimension::L; // line
+    }
+
+    int
+    getBoundaryDimension() const override
+    {
+        return isClosed() ? Dimension::False : 0;
+    }
+
+    virtual bool isClosed() const = 0;
+
+protected:
+
+    Curve(const GeometryFactory& factory) : Geometry(&factory) {}
+
+};
+
+}
+}
diff --git a/include/geos/geom/CurvePolygon.h b/include/geos/geom/CurvePolygon.h
new file mode 100644
index 000000000..548a092eb
--- /dev/null
+++ b/include/geos/geom/CurvePolygon.h
@@ -0,0 +1,54 @@
+/**********************************************************************
+ *
+ * GEOS - Geometry Engine Open Source
+ * http://geos.osgeo.org
+ *
+ * Copyright (C) 2024 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/geom/SurfaceImpl.h>
+
+namespace geos {
+namespace geom {
+
+class GEOS_DLL CurvePolygon : public SurfaceImpl<Curve> {
+    friend class GeometryFactory;
+
+public:
+    ~CurvePolygon() override = default;
+
+    std::unique_ptr<CoordinateSequence> getCoordinates() const override;
+
+    int
+    getSortIndex() const override
+    {
+        return SORTINDEX_CURVEPOLYGON;
+    }
+
+    std::string getGeometryType() const override;
+
+    GeometryTypeId getGeometryTypeId() const override;
+
+    std::unique_ptr<Geometry> getBoundary() const override;
+
+    void normalize() override;
+
+protected:
+    using SurfaceImpl::SurfaceImpl;
+
+    Geometry* cloneImpl() const override;
+
+    Geometry* reverseImpl() const override;
+};
+
+
+}
+}
diff --git a/include/geos/geom/Geometry.h b/include/geos/geom/Geometry.h
index cfa989a9a..cf6cdd510 100644
--- a/include/geos/geom/Geometry.h
+++ b/include/geos/geom/Geometry.h
@@ -86,7 +86,12 @@ enum GeometryTypeId {
     /// a collection of polygons
     GEOS_MULTIPOLYGON,
     /// a collection of heterogeneus geometries
-    GEOS_GEOMETRYCOLLECTION
+    GEOS_GEOMETRYCOLLECTION,
+    GEOS_CIRCULARSTRING,
+    GEOS_COMPOUNDCURVE,
+    GEOS_CURVEPOLYGON,
+    GEOS_MULTICURVE,
+    GEOS_MULTISURFACE,
 };
 
 enum GeometrySortIndex {
@@ -97,7 +102,12 @@ enum GeometrySortIndex {
     SORTINDEX_MULTILINESTRING = 4,
     SORTINDEX_POLYGON = 5,
     SORTINDEX_MULTIPOLYGON = 6,
-    SORTINDEX_GEOMETRYCOLLECTION = 7
+    SORTINDEX_GEOMETRYCOLLECTION = 7,
+    SORTINDEX_CIRCULARSTRING = 8,
+    SORTINDEX_COMPOUNDCURVE = 9,
+    SORTINDEX_CURVEPOLYGON = 10,
+    SORTINDEX_MULTICURVE = 11,
+    SORTINDEX_MULTISURFACE = 12,
 };
 
 /**
@@ -919,11 +929,30 @@ protected:
 
     virtual int compareToSameClass(const Geometry* geom) const = 0; //Abstract
 
-    int compare(std::vector<Coordinate> a, std::vector<Coordinate> b) const;
+    template<typename T>
+    static int compare(const T& a, const T& b)
+    {
+        std::size_t i = 0;
+        std::size_t j = 0;
+        while(i < a.size() && j < b.size()) {
+            const auto& aGeom = *a[i];
+            const auto& bGeom = *b[j];
+        int comparison = aGeom.compareTo(&bGeom);
+        if(comparison != 0) {
+            return comparison;
+        }
+        i++;
+        j++;
+        }
+        if(i < a.size()) {
+        return 1;
+        }
+        if(j < b.size()) {
+        return -1;
+        }
+        return 0;
 
-    int compare(std::vector<Geometry*> a, std::vector<Geometry*> b) const;
-
-    int compare(const std::vector<std::unique_ptr<Geometry>> & a, const std::vector<std::unique_ptr<Geometry>> & b) const;
+    }
 
     bool equal(const CoordinateXY& a, const CoordinateXY& b,
                double tolerance) const;
diff --git a/include/geos/geom/GeometryFactory.h b/include/geos/geom/GeometryFactory.h
index ce2be87ae..f5ae08cc4 100644
--- a/include/geos/geom/GeometryFactory.h
+++ b/include/geos/geom/GeometryFactory.h
@@ -26,6 +26,7 @@
 #include <geos/geom/MultiLineString.h>
 #include <geos/geom/MultiPolygon.h>
 #include <geos/geom/PrecisionModel.h>
+#include <geos/geom/Polygon.h>
 #include <geos/util/IllegalArgumentException.h>
 #include <geos/export.h>
 
@@ -36,6 +37,8 @@
 namespace geos {
 namespace geom {
 class Coordinate;
+class CircularString;
+class CompoundCurve;
 class CoordinateSequence;
 class Envelope;
 class Geometry;
@@ -43,9 +46,11 @@ class GeometryCollection;
 class LineString;
 class LinearRing;
 class MultiLineString;
+class MultiCurve;
 class MultiPoint;
 class MultiPolygon;
-class Polygon;
+class MultiSurface;
+class CurvePolygon;
 }
 }
 
@@ -187,6 +192,16 @@ public:
     std::unique_ptr<MultiLineString> createMultiLineString(
             std::vector<std::unique_ptr<Geometry>> && fromLines) const;
 
+    /// Construct an EMPTY MultiCurve
+    std::unique_ptr<MultiCurve> createMultiCurve() const;
+
+    /// Construct a MultiCurve taking ownership of given arguments
+    std::unique_ptr<MultiCurve> createMultiCurve(
+            std::vector<std::unique_ptr<Geometry>> && fromCurves) const;
+
+    std::unique_ptr<MultiCurve> createMultiCurve(
+            std::vector<std::unique_ptr<Curve>> && fromCurves) const;
+
     /// Construct an EMPTY MultiPolygon
     std::unique_ptr<MultiPolygon> createMultiPolygon() const;
 
@@ -201,6 +216,16 @@ public:
     std::unique_ptr<MultiPolygon> createMultiPolygon(
             std::vector<std::unique_ptr<Geometry>> && fromPolys) const;
 
+    /// Construct an EMPTY MultiSurface
+    std::unique_ptr<MultiSurface> createMultiSurface() const;
+
+    /// Construct a MultiSurface taking ownership of given arguments
+    std::unique_ptr<MultiSurface> createMultiSurface(
+            std::vector<std::unique_ptr<Geometry>> && from) const;
+
+    std::unique_ptr<MultiSurface> createMultiSurface(
+            std::vector<std::unique_ptr<Surface>> && from) const;
+
     /// Construct an EMPTY LinearRing
     std::unique_ptr<LinearRing> createLinearRing(std::size_t coordinateDimension = 2) const;
 
@@ -258,6 +283,13 @@ public:
     Polygon* createPolygon(const LinearRing& shell,
                            const std::vector<LinearRing*>& holes) const;
 
+
+    /// Construct a CurvePolygon taking ownership of given arguments
+    std::unique_ptr<CurvePolygon> createCurvePolygon(std::unique_ptr<Curve>&& shell) const;
+
+    std::unique_ptr<CurvePolygon> createCurvePolygon(std::unique_ptr<Curve>&& shell,
+                                                     std::vector<std::unique_ptr<Curve>> && holes) const;
+
     /// Construct an EMPTY LineString
     std::unique_ptr<LineString> createLineString(std::size_t coordinateDimension = 2) const;
 
@@ -272,6 +304,24 @@ public:
     std::unique_ptr<LineString> createLineString(
         const CoordinateSequence& coordinates) const;
 
+    /// Construct an EMPTY CircularString
+    std::unique_ptr<CircularString> createCircularString(bool hasZ = false, bool hasM = false) const;
+
+    /// Copy a CircularString
+    std::unique_ptr<CircularString> createCircularString(const CircularString& ls) const;
+
+    /// Construct a CircularString taking ownership of given argument
+    std::unique_ptr<CircularString> createCircularString(
+        std::unique_ptr<CoordinateSequence> && coordinates) const;
+
+    /// Construct a CircularString with a deep-copy of given argument
+    std::unique_ptr<CircularString> createCircularString(
+        const CoordinateSequence& coordinates) const;
+
+    std::unique_ptr<CompoundCurve> createCompoundCurve() const;
+
+    std::unique_ptr<CompoundCurve> createCompoundCurve(std::vector<std::unique_ptr<SimpleCurve>>&&) const;
+
     /**
     * Creates an empty atomic geometry of the given dimension.
     * If passed a dimension of -1 will create an empty {@link GeometryCollection}.
diff --git a/include/geos/geom/LineString.h b/include/geos/geom/LineString.h
index 7e70ca5e5..ebcaffc3d 100644
--- a/include/geos/geom/LineString.h
+++ b/include/geos/geom/LineString.h
@@ -25,6 +25,7 @@
 #include <geos/geom/CoordinateSequence.h> // for proper use of unique_ptr<>
 #include <geos/geom/Envelope.h> // for proper use of unique_ptr<>
 #include <geos/geom/Dimension.h> // for Dimension::DimensionType
+#include <geos/geom/SimpleCurve.h>
 
 #include <string>
 #include <vector>
@@ -62,7 +63,7 @@ namespace geom { // geos::geom
  *  If these conditions are not met, the constructors throw
  *  an {@link util::IllegalArgumentException}.
  */
-class GEOS_DLL LineString: public Geometry {
+class GEOS_DLL LineString: public SimpleCurve {
 
 public:
 
@@ -85,109 +86,10 @@ public:
         return std::unique_ptr<LineString>(cloneImpl());
     }
 
-    std::unique_ptr<CoordinateSequence> getCoordinates() const override;
-
-    /// Returns a read-only pointer to internal CoordinateSequence
-    const CoordinateSequence* getCoordinatesRO() const;
-
-    virtual const Coordinate& getCoordinateN(std::size_t n) const;
-
-    /**
-     * \brief
-     * Take ownership of the CoordinateSequence managed by this geometry.
-     * After releasing the coordinates, the geometry should be considered
-     * in a moved-from state and should not be accessed.
-     * @return this Geometry's CoordinateSequence.
-     */
-    std::unique_ptr<CoordinateSequence> releaseCoordinates();
-
-    /// Returns line dimension (1)
-    Dimension::DimensionType getDimension() const override;
-
-    /**
-     * \brief
-     * Returns Dimension::False for a closed LineString,
-     * 0 otherwise (LineString boundary is a MultiPoint)
-     */
-    int getBoundaryDimension() const override;
-
-    /// Returns coordinate dimension.
-    uint8_t getCoordinateDimension() const override;
-
-    bool hasM() const override;
-
-    bool hasZ() const override;
-
-    /**
-     * \brief
-     * Returns a MultiPoint.
-     * Empty for closed LineString, a Point for each vertex otherwise.
-     */
-    std::unique_ptr<Geometry> getBoundary() const override;
-
-    bool isEmpty() const override;
-
-    std::size_t getNumPoints() const override;
-
-    virtual std::unique_ptr<Point> getPointN(std::size_t n) const;
-
-    /// \brief
-    /// Return the start point of the LineString
-    /// or NULL if this is an EMPTY LineString.
-    ///
-    virtual std::unique_ptr<Point> getStartPoint() const;
-
-    /// \brief
-    /// Return the end point of the LineString
-    /// or NULL if this is an EMPTY LineString.
-    ///
-    virtual std::unique_ptr<Point> getEndPoint() const;
-
-    virtual bool isClosed() const;
-
-    virtual bool isRing() const;
-
     std::string getGeometryType() const override;
 
     GeometryTypeId getGeometryTypeId() const override;
 
-    virtual bool isCoordinate(Coordinate& pt) const;
-
-    bool equalsExact(const Geometry* other, double tolerance = 0)
-    const override;
-
-    bool equalsIdentical(const Geometry* other) const override;
-
-    void apply_rw(const CoordinateFilter* filter) override;
-
-    void apply_ro(CoordinateFilter* filter) const override;
-
-    void apply_rw(GeometryFilter* filter) override;
-
-    void apply_ro(GeometryFilter* filter) const override;
-
-    void apply_rw(GeometryComponentFilter* filter) override;
-
-    void apply_ro(GeometryComponentFilter* filter) const override;
-
-    void apply_rw(CoordinateSequenceFilter& filter) override;
-
-    void apply_ro(CoordinateSequenceFilter& filter) const override;
-
-    /** \brief
-     * Normalizes a LineString.
-     *
-     * A normalized linestring
-     * has the first point which is not equal to its reflected point
-     * less than the reflected point.
-     */
-    void normalize() override;
-
-    //was protected
-    int compareToSameClass(const Geometry* ls) const override;
-
-    const CoordinateXY* getCoordinate() const override;
-
     double getLength() const override;
 
     /**
@@ -198,10 +100,6 @@ public:
      */
     std::unique_ptr<LineString> reverse() const { return std::unique_ptr<LineString>(reverseImpl()); }
 
-    const Envelope* getEnvelopeInternal() const override {
-        return &envelope;
-    }
-
 protected:
 
     LineString(const LineString& ls);
@@ -216,28 +114,15 @@ protected:
 
     LineString* reverseImpl() const override;
 
-    Envelope computeEnvelopeInternal() const;
-
-    CoordinateSequence::Ptr points;
-
-    mutable Envelope envelope;
-
     int
     getSortIndex() const override
     {
         return SORTINDEX_LINESTRING;
     };
 
-    void geometryChangedAction() override {
-        envelope = computeEnvelopeInternal();
-    }
-
 private:
 
     void validateConstruction();
-    void normalizeClosed();
-
-
 };
 
 struct GEOS_DLL  LineStringLT {
diff --git a/include/geos/geom/MultiCurve.h b/include/geos/geom/MultiCurve.h
new file mode 100644
index 000000000..f626bcdf0
--- /dev/null
+++ b/include/geos/geom/MultiCurve.h
@@ -0,0 +1,124 @@
+/**********************************************************************
+ *
+ * GEOS - Geometry Engine Open Source
+ * http://geos.osgeo.org
+ *
+ * Copyright (C) 2024 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/geom/Curve.h>
+#include <geos/geom/GeometryCollection.h>
+
+namespace geos {
+namespace geom {
+
+class GEOS_DLL MultiCurve : public GeometryCollection {
+    friend class GeometryFactory;
+
+public:
+    ~MultiCurve() override = default;
+
+    /// Returns line dimension (1)
+    Dimension::DimensionType getDimension() const override;
+
+    bool hasDimension(Dimension::DimensionType d) const override
+    {
+        return d == Dimension::L;
+    }
+
+    bool isDimensionStrict(Dimension::DimensionType d) const override
+    {
+        return d == Dimension::L;
+    }
+
+    /**
+     * \brief
+     * Returns Dimension::False if all [Curves](@ref geom::Curve) in the collection
+     * are closed, 0 otherwise.
+     */
+    int getBoundaryDimension() const override;
+
+    /// Returns a (possibly empty) [MultiPoint](@ref geom::MultiPoint)
+    std::unique_ptr<Geometry> getBoundary() const override;
+
+    const Curve* getGeometryN(std::size_t n) const override;
+
+    std::string getGeometryType() const override;
+
+    GeometryTypeId getGeometryTypeId() const override;
+
+    bool isClosed() const;
+
+    std::unique_ptr<MultiCurve> clone() const
+    {
+        return std::unique_ptr<MultiCurve>(cloneImpl());
+    };
+
+    /**
+     * Creates a MultiCurve in the reverse
+     * order to this object.
+     * Both the order of the component Curves
+     * and the order of their coordinate sequences
+     * are reversed.
+     *
+     * @return a MultiCurve in the reverse order
+     */
+    std::unique_ptr<MultiCurve> reverse() const
+    {
+        return std::unique_ptr<MultiCurve>(reverseImpl());
+    }
+
+protected:
+
+    /**
+     * \brief Constructs a MultiCurve.
+     *
+     * @param  newLines The [Curves](@ref geom::Curve) for this
+     *                  MultiCurve, or `null`
+     *                  or an empty array to create the empty geometry.
+     *                  Elements may be empty Curve,
+     *                  but not `null`s.
+     *
+     * @param newFactory The GeometryFactory used to create this geometry.
+     *                   Caller must keep the factory alive for the life-time
+     *                   of the constructed MultiCurve.
+     *
+     * @note Constructed object will take ownership of
+     *       the vector and its elements.
+     *
+     */
+    MultiCurve(std::vector<std::unique_ptr<Curve>>&& newLines,
+               const GeometryFactory& newFactory);
+
+    MultiCurve(std::vector<std::unique_ptr<Geometry>>&& newLines,
+               const GeometryFactory& newFactory);
+
+    MultiCurve(const MultiCurve& mp)
+        : GeometryCollection(mp)
+    {}
+
+    MultiCurve* cloneImpl() const override
+    {
+        return new MultiCurve(*this);
+    }
+
+    MultiCurve* reverseImpl() const override;
+
+    int
+    getSortIndex() const override
+    {
+        return SORTINDEX_MULTICURVE;
+    };
+
+};
+
+}
+}
diff --git a/include/geos/geom/MultiPolygon.h b/include/geos/geom/MultiPolygon.h
index 391b71240..ab5b27d50 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/MultiPolygon.h>
 #include <geos/geom/GeometryCollection.h>
 
 
@@ -35,6 +34,7 @@ namespace geos {
 namespace geom { // geos::geom
 class Coordinate;
 class MultiPoint;
+class Polygon;
 }
 }
 
diff --git a/include/geos/geom/MultiSurface.h b/include/geos/geom/MultiSurface.h
new file mode 100644
index 000000000..af47a947e
--- /dev/null
+++ b/include/geos/geom/MultiSurface.h
@@ -0,0 +1,94 @@
+/**********************************************************************
+ *
+ * GEOS - Geometry Engine Open Source
+ * http://geos.osgeo.org
+ *
+ * Copyright (C) 2024 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/geom/GeometryCollection.h>
+#include <geos/geom/Surface.h>
+
+namespace geos {
+namespace geom {
+class GEOS_DLL MultiSurface : public GeometryCollection {
+    friend class GeometryFactory;
+
+public:
+
+    ~MultiSurface() override;
+
+    /// Returns surface dimension (2)
+    Dimension::DimensionType getDimension() const override;
+
+    bool hasDimension(Dimension::DimensionType d) const override
+    {
+        return d == Dimension::A;
+    }
+
+    bool isDimensionStrict(Dimension::DimensionType d) const override
+    {
+        return d == Dimension::A;
+    }
+
+    /// Returns 1 (MultiSurface boundary is MultiCurve)
+    int getBoundaryDimension() const override;
+
+    /** \brief
+     * Computes the boundary of this geometry
+     *
+     * @return a lineal geometry (which may be empty)
+     * @see Geometry#getBoundary
+     */
+    std::unique_ptr<Geometry> getBoundary() const override;
+
+    std::string getGeometryType() const override;
+
+    GeometryTypeId getGeometryTypeId() const override;
+
+    std::unique_ptr<MultiSurface> clone() const
+    {
+        return std::unique_ptr<MultiSurface>(cloneImpl());
+    };
+
+    std::unique_ptr<MultiSurface> reverse() const
+    {
+        return std::unique_ptr<MultiSurface>(reverseImpl());
+    }
+
+protected:
+
+    MultiSurface(std::vector<std::unique_ptr<Geometry>>&& newPolys,
+                 const GeometryFactory& newFactory);
+
+    MultiSurface(std::vector<std::unique_ptr<Surface>>&& newPolys,
+                 const GeometryFactory& newFactory);
+
+    MultiSurface(const MultiSurface& mp)
+        : GeometryCollection(mp)
+    {};
+
+    MultiSurface* cloneImpl() const override
+    {
+        return new MultiSurface(*this);
+    }
+
+    MultiSurface* reverseImpl() const override;
+
+    int
+    getSortIndex() const override
+    {
+        return SORTINDEX_MULTISURFACE;
+    };
+
+};
+}
+}
diff --git a/include/geos/geom/Polygon.h b/include/geos/geom/Polygon.h
index abd5f100f..95936b52f 100644
--- a/include/geos/geom/Polygon.h
+++ b/include/geos/geom/Polygon.h
@@ -27,6 +27,7 @@
 #include <geos/geom/Envelope.h> // for proper use of unique_ptr<>
 #include <geos/geom/LinearRing.h>
 #include <geos/geom/Dimension.h> // for Dimension::DimensionType
+#include <geos/geom/SurfaceImpl.h>
 
 #include <memory> // for unique_ptr
 
@@ -57,7 +58,7 @@ namespace geom { // geos::geom
  *  Specification for SQL</A> .
  *
  */
-class GEOS_DLL Polygon: public Geometry {
+class GEOS_DLL Polygon: public SurfaceImpl<LinearRing> {
 
 public:
 
@@ -68,6 +69,9 @@ public:
 
     ~Polygon() override = default;
 
+    std::unique_ptr<CoordinateSequence>
+    getCoordinates() const override;
+
     /**
      * Creates and returns a full copy of this {@link Polygon} object.
      * (including all coordinates contained by it).
@@ -79,23 +83,6 @@ public:
         return std::unique_ptr<Polygon>(cloneImpl());
     }
 
-    std::unique_ptr<CoordinateSequence> getCoordinates() const override;
-
-    std::size_t getNumPoints() const override;
-
-    /// Returns surface dimension (2)
-    Dimension::DimensionType getDimension() const override;
-
-    /// Returns coordinate dimension.
-    uint8_t getCoordinateDimension() const override;
-
-    bool hasM() const override;
-
-    bool hasZ() const override;
-
-    /// Returns 1 (Polygon boundary is a MultiLineString)
-    int getBoundaryDimension() const override;
-
     /** \brief
      * Computes the boundary of this geometry
      *
@@ -104,68 +91,13 @@ public:
      */
     std::unique_ptr<Geometry> getBoundary() const override;
 
-    bool isEmpty() const override;
-
-    /// Returns the exterior ring (shell)
-    const LinearRing* getExteriorRing() const;
-
-    /**
-     * \brief
-     * Take ownership of this Polygon's exterior ring.
-     * After releasing the exterior ring, the Polygon should be
-     * considered in a moved-from state and should not be accessed,
-     * except to release the interior rings (if desired.)
-     * @return exterior ring
-     */
-    std::unique_ptr<LinearRing> releaseExteriorRing();
-
-    /// Returns number of interior rings (hole)
-    std::size_t getNumInteriorRing() const;
-
-    /// Get nth interior ring (hole)
-    const LinearRing* getInteriorRingN(std::size_t n) const;
-
-    /**
-     * \brief
-     * Take ownership of this Polygon's interior rings.
-     * After releasing the rings, the Polygon should be
-     * considered in a moved-from state and should not be accessed,
-     * except to release the exterior ring (if desired.)
-     * @return vector of rings (may be empty)
-     */
-    std::vector<std::unique_ptr<LinearRing>> releaseInteriorRings();
-
     std::string getGeometryType() const override;
     GeometryTypeId getGeometryTypeId() const override;
-    bool equalsExact(const Geometry* other, double tolerance = 0) const override;
-    bool equalsIdentical(const Geometry* other) const override;
-    void apply_rw(const CoordinateFilter* filter) override;
-    void apply_ro(CoordinateFilter* filter) const override;
-    void apply_rw(GeometryFilter* filter) override;
-    void apply_ro(GeometryFilter* filter) const override;
-    void apply_rw(CoordinateSequenceFilter& filter) override;
-    void apply_ro(CoordinateSequenceFilter& filter) const override;
-    void apply_rw(GeometryComponentFilter* filter) override;
-    void apply_ro(GeometryComponentFilter* filter) const override;
-
-    std::unique_ptr<Geometry> convexHull() const override;
 
     void normalize() override;
 
-    /**
-     * \brief
-     * Apply a ring ordering convention to this polygon, with
-     * interior rings having an opposite orientation to the
-     * specified exterior orientation.
-     *
-     * \param exteriorCW should exterior ring be clockwise?
-     */
-    void orientRings(bool exteriorCW);
-
     std::unique_ptr<Polygon> reverse() const { return std::unique_ptr<Polygon>(reverseImpl()); }
 
-    const CoordinateXY* getCoordinate() const override;
-
     double getArea() const override;
 
     /// Returns the perimeter of this <code>Polygon</code>
@@ -173,52 +105,24 @@ public:
 
     bool isRectangle() const override;
 
-    const Envelope* getEnvelopeInternal() const override {
-        return shell->getEnvelopeInternal();
-    }
+    /**
+    * \brief
+    * Apply a ring ordering convention to this polygon, with
+    * interior rings having an opposite orientation to the
+    * specified exterior orientation.
+    *
+    * \param exteriorCW should exterior ring be clockwise?
+    */
+    void orientRings(bool exteriorCW);
 
 protected:
 
-
-    Polygon(const Polygon& p);
-
-    int compareToSameClass(const Geometry* p) const override;
-
-    /**
-     * Constructs a <code>Polygon</code> with the given exterior
-     * and interior boundaries.
-     *
-     * @param  newShell  the outer boundary of the new Polygon,
-     *                   or <code>null</code> or an empty
-     *		     LinearRing if the empty geometry
-     *                   is to be created.
-     *
-     * @param  newHoles  the LinearRings defining the inner
-     *                   boundaries of the new Polygon, or
-     *                   null or empty LinearRing
-     *                   if the empty  geometry is to be created.
-     *
-     * @param newFactory the GeometryFactory used to create this geometry
-     *
-     * Polygon will take ownership of Shell and Holes LinearRings
-     */
-    Polygon(std::unique_ptr<LinearRing> && newShell,
-            std::vector<std::unique_ptr<LinearRing>> && newHoles,
-            const GeometryFactory& newFactory);
-
-    Polygon(std::unique_ptr<LinearRing> && newShell,
-            const GeometryFactory& newFactory);
+    using SurfaceImpl::SurfaceImpl;
 
     Polygon* cloneImpl() const override { return new Polygon(*this); }
 
     Polygon* reverseImpl() const override;
 
-    std::unique_ptr<LinearRing> shell;
-
-    std::vector<std::unique_ptr<LinearRing>> holes;
-
-    void geometryChangedAction() override {}
-
     int
     getSortIndex() const override
     {
diff --git a/include/geos/geom/LineString.h b/include/geos/geom/SimpleCurve.h
similarity index 50%
copy from include/geos/geom/LineString.h
copy to include/geos/geom/SimpleCurve.h
index 7e70ca5e5..35e226ec8 100644
--- a/include/geos/geom/LineString.h
+++ b/include/geos/geom/SimpleCurve.h
@@ -3,88 +3,29 @@
  * GEOS - Geometry Engine Open Source
  * http://geos.osgeo.org
  *
- * Copyright (C) 2011 Sandro Santilli <strk at kbt.io>
  * Copyright (C) 2001-2002 Vivid Solutions Inc.
  * Copyright (C) 2005 2006 Refractions Research Inc.
+ * Copyright (C) 2011 Sandro Santilli <strk at kbt.io>
+ * Copyright (C) 2024 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.
  *
- **********************************************************************
- *
- * Last port: geom/LineString.java r320 (JTS-1.12)
- *
  **********************************************************************/
 
 #pragma once
 
-#include <geos/export.h>
-#include <geos/geom/Geometry.h> // for inheritance
-#include <geos/geom/CoordinateSequence.h> // for proper use of unique_ptr<>
-#include <geos/geom/Envelope.h> // for proper use of unique_ptr<>
-#include <geos/geom/Dimension.h> // for Dimension::DimensionType
-
-#include <string>
-#include <vector>
-#include <memory> // for unique_ptr
-
-
-#ifdef _MSC_VER
-#pragma warning(push)
-#pragma warning(disable: 4251) // warning C4251: needs to have dll-interface to be used by clients of class
-#endif
+#include <geos/geom/Curve.h>
+#include <geos/geom/Geometry.h>
 
 namespace geos {
 namespace geom {
-class Coordinate;
-class CoordinateSequenceFilter;
-}
-}
-
-namespace geos {
-namespace geom { // geos::geom
-
-/**
- *  Models an OGC-style <code>LineString</code>.
- *
- *  A LineString consists of a sequence of two or more vertices,
- *  along with all points along the linearly-interpolated curves
- *  (line segments) between each
- *  pair of consecutive vertices.
- *  Consecutive vertices may be equal.
- *  The line segments in the line may intersect each other (in other words,
- *  the linestring may "curl back" in itself and self-intersect).
- *  Linestrings with exactly two identical points are invalid.
- *
- *  A linestring must have either 0 or 2 or more points.
- *  If these conditions are not met, the constructors throw
- *  an {@link util::IllegalArgumentException}.
- */
-class GEOS_DLL LineString: public Geometry {
 
+class GEOS_DLL SimpleCurve : public Curve {
 public:
 
-    friend class GeometryFactory;
-
-    /// A vector of const LineString pointers
-    typedef std::vector<const LineString*> ConstVect;
-
-    ~LineString() override;
-
-    /**
-     * \brief
-     * Creates and returns a full copy of this {@link LineString} object
-     * (including all coordinates contained by it)
-     *
-     * @return A clone of this instance
-     */
-    std::unique_ptr<LineString> clone() const
-    {
-        return std::unique_ptr<LineString>(cloneImpl());
-    }
-
     std::unique_ptr<CoordinateSequence> getCoordinates() const override;
 
     /// Returns a read-only pointer to internal CoordinateSequence
@@ -101,9 +42,6 @@ public:
      */
     std::unique_ptr<CoordinateSequence> releaseCoordinates();
 
-    /// Returns line dimension (1)
-    Dimension::DimensionType getDimension() const override;
-
     /**
      * \brief
      * Returns Dimension::False for a closed LineString,
@@ -143,13 +81,9 @@ public:
     ///
     virtual std::unique_ptr<Point> getEndPoint() const;
 
-    virtual bool isClosed() const;
+    bool isClosed() const override;
 
-    virtual bool isRing() const;
-
-    std::string getGeometryType() const override;
-
-    GeometryTypeId getGeometryTypeId() const override;
+    bool isRing() const;
 
     virtual bool isCoordinate(Coordinate& pt) const;
 
@@ -158,6 +92,13 @@ public:
 
     bool equalsIdentical(const Geometry* other) const override;
 
+    const CoordinateXY* getCoordinate() const override;
+
+    const Envelope* getEnvelopeInternal() const override
+    {
+        return &envelope;
+    }
+
     void apply_rw(const CoordinateFilter* filter) override;
 
     void apply_ro(CoordinateFilter* filter) const override;
@@ -175,9 +116,9 @@ public:
     void apply_ro(CoordinateSequenceFilter& filter) const override;
 
     /** \brief
-     * Normalizes a LineString.
+     * Normalizes a SimpleCurve.
      *
-     * A normalized linestring
+     * A normalized simple curve
      * has the first point which is not equal to its reflected point
      * less than the reflected point.
      */
@@ -186,72 +127,30 @@ public:
     //was protected
     int compareToSameClass(const Geometry* ls) const override;
 
-    const CoordinateXY* getCoordinate() const override;
-
-    double getLength() const override;
-
-    /**
-     * Creates a LineString whose coordinates are in the reverse
-     * order of this object's
-     *
-     * @return a LineString with coordinates in the reverse order
-     */
-    std::unique_ptr<LineString> reverse() const { return std::unique_ptr<LineString>(reverseImpl()); }
-
-    const Envelope* getEnvelopeInternal() const override {
-        return &envelope;
-    }
 
 protected:
 
-    LineString(const LineString& ls);
+    SimpleCurve(const SimpleCurve& other);
 
-    /// \brief
-    /// Constructs a LineString taking ownership the
-    /// given CoordinateSequence.
-    LineString(CoordinateSequence::Ptr && pts,
-               const GeometryFactory& newFactory);
-
-    LineString* cloneImpl() const override { return new LineString(*this); }
-
-    LineString* reverseImpl() const override;
+    SimpleCurve(std::unique_ptr<CoordinateSequence>&& newCoords,
+                const GeometryFactory& factory);
 
     Envelope computeEnvelopeInternal() const;
 
-    CoordinateSequence::Ptr points;
-
-    mutable Envelope envelope;
-
-    int
-    getSortIndex() const override
+    void geometryChangedAction() override
     {
-        return SORTINDEX_LINESTRING;
-    };
-
-    void geometryChangedAction() override {
         envelope = computeEnvelopeInternal();
     }
 
+    // TODO: hold value or shared_ptr instead of unique_ptr
+    std::unique_ptr<CoordinateSequence> points;
+
+    mutable Envelope envelope;
+
 private:
 
-    void validateConstruction();
     void normalizeClosed();
-
-
 };
 
-struct GEOS_DLL  LineStringLT {
-    bool
-    operator()(const LineString* ls1, const LineString* ls2) const
-    {
-        return ls1->compareTo(ls2) < 0;
-    }
-};
-
-} // namespace geos::geom
-} // namespace geos
-
-#ifdef _MSC_VER
-#pragma warning(pop)
-#endif
-
+}
+}
diff --git a/include/geos/geom/Surface.h b/include/geos/geom/Surface.h
new file mode 100644
index 000000000..f6d66c8ee
--- /dev/null
+++ b/include/geos/geom/Surface.h
@@ -0,0 +1,118 @@
+/**********************************************************************
+ *
+ * GEOS - Geometry Engine Open Source
+ * http://geos.osgeo.org
+ *
+ * Copyright (C) 2024 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/geom/Geometry.h>
+
+namespace geos {
+namespace geom {
+
+class Curve;
+
+class GEOS_DLL Surface : public Geometry {
+
+private:
+
+protected:
+    using Geometry::Geometry;
+
+public:
+
+    uint8_t getCoordinateDimension() const override;
+
+    /// Returns the exterior ring (shell)
+    virtual const Curve* getExteriorRing() const = 0;
+
+    /// Returns number of interior rings (holes)
+    virtual size_t getNumInteriorRing() const = 0;
+
+    /// Get nth interior ring (hole)
+    virtual const Curve* getInteriorRingN(std::size_t n) const = 0;
+
+    size_t getNumPoints() const override;
+
+
+    bool
+    equalsExact(const Geometry* other, double tolerance) const override;
+
+    bool
+    equalsIdentical(const Geometry* other) const override;
+
+
+    std::unique_ptr<Geometry> convexHull() const override;
+
+    int
+    getBoundaryDimension() const override
+    {
+        return 1;
+    }
+
+    const CoordinateXY* getCoordinate() const override;
+
+    Dimension::DimensionType
+    getDimension() const override
+    {
+        return Dimension::A; // area
+    }
+
+    const Envelope* getEnvelopeInternal() const override;
+
+    bool hasM() const override;
+
+    bool hasZ() const override;
+
+    bool isEmpty() const override;
+
+    void
+    apply_ro(CoordinateFilter* filter) const override;
+
+    void
+    apply_rw(const CoordinateFilter* filter) override;
+
+    void
+    apply_rw(GeometryFilter* filter) override;
+
+    void
+    apply_ro(GeometryFilter* filter) const override;
+
+    void
+    apply_ro(GeometryComponentFilter* filter) const override;
+
+    void
+    apply_rw(GeometryComponentFilter* filter) override;
+
+    void
+    apply_rw(CoordinateSequenceFilter& filter) override;
+
+    void
+    apply_ro(CoordinateSequenceFilter& filter) const override;
+
+protected:
+
+    int
+    compareToSameClass(const Geometry* g) const override;
+
+    void geometryChangedAction() override {}
+
+    static std::unique_ptr<Geometry> createEmptyRing(const GeometryFactory&);
+
+    virtual Curve* getExteriorRing() = 0;
+
+    virtual Curve* getInteriorRingN(std::size_t i) = 0;
+
+};
+
+}
+}
diff --git a/include/geos/geom/SurfaceImpl.h b/include/geos/geom/SurfaceImpl.h
new file mode 100644
index 000000000..0fd917dba
--- /dev/null
+++ b/include/geos/geom/SurfaceImpl.h
@@ -0,0 +1,161 @@
+/**********************************************************************
+ *
+ * GEOS - Geometry Engine Open Source
+ * http://geos.osgeo.org
+ *
+ * Copyright (C) 2024 ISciences, LLC
+ * Copyright (C) 2011 Sandro Santilli <strk at kbt.io>
+ * Copyright (C) 2005 2006 Refractions Research Inc.
+ * Copyright (C) 2001-2002 Vivid Solutions Inc.
+ *
+ * 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/geom/Curve.h>
+#include <geos/geom/Surface.h>
+#include <geos/geom/CoordinateSequenceFilter.h>
+#include <geos/geom/GeometryComponentFilter.h>
+#include <geos/geom/GeometryFilter.h>
+#include <geos/geom/LinearRing.h>
+#include <geos/util/IllegalArgumentException.h>
+
+namespace geos {
+namespace geom {
+
+template<typename RingType>
+class SurfaceImpl : public Surface {
+
+protected:
+
+    SurfaceImpl(const SurfaceImpl& p)
+        :
+        Surface(p),
+        shell(static_cast<RingType*>(p.shell->clone().release())),
+        holes(p.holes.size())
+    {
+        for (std::size_t i = 0; i < holes.size(); ++i) {
+            holes[i].reset(static_cast<RingType*>(p.holes[i]->clone().release()));
+        }
+    }
+
+    /**
+     * Constructs a <code>Surface</code> with the given exterior
+     * and interior boundaries.
+     *
+     * @param  newShell  the outer boundary of the new Polygon,
+     *                   or <code>null</code> or an empty
+             *		     Curve if the empty geometry
+     *                   is to be created.
+     *
+     * @param  newHoles  the rings defining the inner
+     *                   boundaries of the new Surface, or
+     *                   null or empty Curve
+     *                   if the empty geometry is to be created.
+     *
+     * @param newFactory the GeometryFactory used to create this geometry
+     *
+     * Polygon will take ownership of shell and hole curves
+     */
+    SurfaceImpl(std::unique_ptr<RingType>&& newShell,
+                const GeometryFactory& newFactory) :
+        Surface(&newFactory),
+        shell(std::move(newShell))
+    {
+        if (shell == nullptr) {
+            shell.reset(static_cast<RingType*>(createEmptyRing(newFactory).release()));
+        }
+    }
+
+    SurfaceImpl(std::unique_ptr<RingType>&& newShell,
+                std::vector<std::unique_ptr<RingType>>&& newHoles,
+                const GeometryFactory& newFactory) :
+        Surface(&newFactory),
+        shell(std::move(newShell)),
+        holes(std::move(newHoles))
+    {
+        if (shell == nullptr) {
+            shell.reset(static_cast<RingType*>(createEmptyRing(newFactory).release()));
+        }
+
+        if(shell->isEmpty() && hasNonEmptyElements(&holes)) {
+            throw geos::util::IllegalArgumentException("shell is empty but holes are not");
+        }
+        if (hasNullElements(&holes)) {
+            throw geos::util::IllegalArgumentException("holes must not contain null elements");
+        }
+    }
+
+public:
+
+
+    const RingType*
+    getExteriorRing() const override
+    {
+        return shell.get();
+    }
+
+    RingType*
+    getExteriorRing() override
+    {
+        return shell.get();
+    }
+
+    /**
+    * \brief
+    * Take ownership of this Polygon's exterior ring.
+    * After releasing the exterior ring, the Polygon should be
+    * considered in a moved-from state and should not be accessed,
+    * except to release the interior rings (if desired.)
+    * @return exterior ring
+    */
+    std::unique_ptr<RingType>
+    releaseExteriorRing()
+    {
+        return std::move(shell);
+    }
+
+    size_t getNumInteriorRing() const override
+    {
+        return holes.size();
+    }
+
+    const RingType*
+    getInteriorRingN(std::size_t n) const override
+    {
+        return holes[n].get();
+    }
+
+    RingType*
+    getInteriorRingN(std::size_t n) override
+    {
+        return holes[n].get();
+    }
+
+    /**
+    * \brief
+    * Take ownership of this Polygon's interior rings.
+    * After releasing the rings, the Polygon should be
+    * considered in a moved-from state and should not be accessed,
+    * except to release the exterior ring (if desired.)
+    * @return vector of rings (may be empty)
+    */
+    std::vector<std::unique_ptr<RingType>>
+                                        releaseInteriorRings()
+    {
+        return std::move(holes);
+    }
+
+protected:
+    std::unique_ptr<RingType> shell;
+    std::vector<std::unique_ptr<RingType>> holes;
+
+};
+
+}
+}
diff --git a/include/geos/io/WKTReader.h b/include/geos/io/WKTReader.h
index 93249bd71..6f9afe216 100644
--- a/include/geos/io/WKTReader.h
+++ b/include/geos/io/WKTReader.h
@@ -40,10 +40,15 @@ class GeometryCollection;
 class Point;
 class LineString;
 class LinearRing;
+class CircularString;
+class CompoundCurve;
 class Polygon;
+class CurvePolygon;
 class MultiPoint;
 class MultiLineString;
+class MultiCurve;
 class MultiPolygon;
+class MultiSurface;
 class PrecisionModel;
 }
 }
@@ -127,6 +132,15 @@ protected:
     std::unique_ptr<geom::MultiLineString> readMultiLineStringText(io::StringTokenizer* tokenizer, OrdinateSet& ordinateFlags) const;
     std::unique_ptr<geom::MultiPolygon> readMultiPolygonText(io::StringTokenizer* tokenizer, OrdinateSet& ordinateFlags) const;
     std::unique_ptr<geom::GeometryCollection> readGeometryCollectionText(io::StringTokenizer* tokenizer, OrdinateSet& ordinateFlags) const;
+
+    std::unique_ptr<geom::CircularString> readCircularStringText(io::StringTokenizer* tokenizer, OrdinateSet& ordinateFlags) const;
+    std::unique_ptr<geom::CompoundCurve> readCompoundCurveText(io::StringTokenizer* tokenizer, OrdinateSet& ordinateFlags) const;
+    std::unique_ptr<geom::CurvePolygon> readCurvePolygonText(io::StringTokenizer* tokenizer, OrdinateSet& ordinateFlags) const;
+    std::unique_ptr<geom::MultiCurve> readMultiCurveText(io::StringTokenizer* tokenizer, OrdinateSet& ordinateFlags) const;
+    std::unique_ptr<geom::MultiSurface> readMultiSurfaceText(io::StringTokenizer* tokenizer, OrdinateSet& ordinateFlags) const;
+
+    std::unique_ptr<geom::Curve> readCurveText(io::StringTokenizer* tokenizer, OrdinateSet& ordinateFlags) const;
+    std::unique_ptr<geom::Geometry> readSurfaceText(io::StringTokenizer* tokenizer, OrdinateSet& ordinateFlags) const;
 private:
     const geom::GeometryFactory* geometryFactory;
     const geom::PrecisionModel* precisionModel;
diff --git a/src/geom/CircularString.cpp b/src/geom/CircularString.cpp
new file mode 100644
index 000000000..fa3962623
--- /dev/null
+++ b/src/geom/CircularString.cpp
@@ -0,0 +1,57 @@
+/**********************************************************************
+ *
+ * GEOS - Geometry Engine Open Source
+ * http://geos.osgeo.org
+ *
+ * Copyright (C) 2024 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/geom/CircularString.h>
+#include <geos/geom/CoordinateSequence.h>
+#include <geos/geom/GeometryFactory.h>
+
+namespace geos {
+namespace geom {
+
+CircularString::~CircularString() = default;
+
+std::string
+CircularString::getGeometryType() const
+{
+    return "CircularString";
+}
+
+GeometryTypeId
+CircularString::getGeometryTypeId() const
+{
+    return GEOS_CIRCULARSTRING;
+}
+
+std::unique_ptr<CircularString>
+CircularString::clone() const
+{
+    return std::unique_ptr<CircularString>(cloneImpl());
+}
+
+CircularString*
+CircularString::reverseImpl() const
+{
+    if (isEmpty()) {
+        return clone().release();
+    }
+
+    assert(points.get());
+    auto seq = points->clone();
+    seq->reverse();
+    assert(getFactory());
+    return getFactory()->createCircularString(std::move(seq)).release();
+}
+
+}
+}
diff --git a/src/geom/CompoundCurve.cpp b/src/geom/CompoundCurve.cpp
new file mode 100644
index 000000000..40510dc7c
--- /dev/null
+++ b/src/geom/CompoundCurve.cpp
@@ -0,0 +1,315 @@
+/**********************************************************************
+ *
+ * GEOS - Geometry Engine Open Source
+ * http://geos.osgeo.org
+ *
+ * Copyright (C) 2024 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/geom/CompoundCurve.h>
+#include <geos/geom/GeometryFactory.h>
+#include <geos/operation/BoundaryOp.h>
+#include <geos/util.h>
+
+namespace geos {
+namespace geom {
+
+CompoundCurve::CompoundCurve(std::vector<std::unique_ptr<SimpleCurve>>&& p_curves,
+                             const GeometryFactory& gf)
+    : Curve(gf),
+      curves(std::move(p_curves)) {}
+
+CompoundCurve::CompoundCurve(const CompoundCurve& other)
+    : Curve(other),
+      curves(other.curves.size()),
+      envelope(other.envelope)
+{
+    for (std::size_t i = 0; i < curves.size(); i++) {
+        curves[i].reset(static_cast<SimpleCurve*>(other.curves[i]->clone().release()));
+    }
+}
+
+CompoundCurve&
+CompoundCurve::operator=(const CompoundCurve& other)
+{
+    curves.resize(other.curves.size());
+    envelope = other.envelope;
+
+    for (std::size_t i =0; i < curves.size(); i++) {
+        curves[i].reset(static_cast<SimpleCurve*>(other.curves[i]->clone().release()));
+    }
+
+    return *this;
+}
+
+std::unique_ptr<CoordinateSequence>
+CompoundCurve::getCoordinates() const
+{
+    // FIXME: CoordinateSequence would have curved and linear sections?
+    auto ret = std::make_unique<CoordinateSequence>(0, hasZ(), hasM());
+    for (const auto& curve : curves) {
+        ret->add(*curve->getCoordinatesRO());
+    }
+    return ret;
+}
+
+const CoordinateXY*
+CompoundCurve::getCoordinate() const
+{
+    for (const auto& curve : curves) {
+        if (!curve->isEmpty()) {
+            return curve->getCoordinate();
+        }
+    }
+
+    return nullptr;
+}
+
+uint8_t
+CompoundCurve::getCoordinateDimension() const
+{
+    return static_cast<std::uint8_t>(2 + hasZ() + hasM());
+}
+
+bool
+CompoundCurve::hasZ() const
+{
+    return std::any_of(curves.begin(), curves.end(), [](const auto& curve) {
+        return curve->hasZ();
+    });
+}
+
+bool
+CompoundCurve::hasM() const
+{
+    return std::any_of(curves.begin(), curves.end(), [](const auto& curve) {
+        return curve->hasM();
+    });
+}
+
+bool
+CompoundCurve::isEmpty() const
+{
+    return !std::any_of(curves.begin(), curves.end(), [](const auto& curve) {
+        return !curve->isEmpty();
+    });
+}
+
+bool
+CompoundCurve::isClosed() const
+{
+    if (isEmpty()) {
+        return false;
+    }
+
+    const SimpleCurve& first = *curves.front();
+    const SimpleCurve& last = *curves.back();
+
+    return first.getCoordinateN(0) == last.getCoordinateN(last.getNumPoints() - 1);
+}
+
+std::size_t
+CompoundCurve::getNumPoints() const
+{
+    std::size_t n =0;
+    for (const auto& curve : curves) {
+        n += curve->getNumPoints();
+    }
+    return n;
+}
+
+std::size_t
+CompoundCurve::getNumCurves() const
+{
+    return curves.size();
+}
+
+std::size_t
+CompoundCurve::getNumGeometries() const
+{
+    return curves.size();
+}
+
+const SimpleCurve*
+CompoundCurve::getCurveN(std::size_t i) const
+{
+    return curves[i].get();
+}
+
+const Geometry*
+CompoundCurve::getGeometryN(std::size_t i) const
+{
+    return curves[i].get();
+}
+
+std::unique_ptr<Geometry>
+CompoundCurve::getBoundary() const
+{
+    operation::BoundaryOp bop(*this);
+    return bop.getBoundary();
+}
+
+
+std::string
+CompoundCurve::getGeometryType() const
+{
+    return "CompoundCurve";
+}
+
+GeometryTypeId
+CompoundCurve::getGeometryTypeId() const
+{
+    return GEOS_COMPOUNDCURVE;
+}
+
+bool
+CompoundCurve::equalsExact(const Geometry* other, double tolerance) const
+{
+    if (!isEquivalentClass(other)) {
+        return false;
+    }
+
+    const CompoundCurve* otherCurve = static_cast<const CompoundCurve*>(other);
+    if (curves.size() != otherCurve->curves.size()) {
+        return false;
+    }
+
+    for (std::size_t i = 0; i < otherCurve->curves.size(); i++) {
+        if (!curves[i]->equalsExact(otherCurve->curves[i].get(), tolerance)) {
+            return false;
+        }
+    }
+
+    return true;
+}
+
+bool
+CompoundCurve::equalsIdentical(const Geometry* other) const
+{
+    if (!isEquivalentClass(other)) {
+        return false;
+    }
+
+    const CompoundCurve* otherCurve = static_cast<const CompoundCurve*>(other);
+    if (curves.size() != otherCurve->curves.size()) {
+        return false;
+    }
+
+    for (std::size_t i = 0; i < otherCurve->curves.size(); i++) {
+        if (!curves[i]->equalsIdentical(otherCurve->curves[i].get())) {
+            return false;
+        }
+    }
+
+    return true;
+}
+
+std::unique_ptr<CompoundCurve>
+CompoundCurve::clone() const
+{
+    return std::unique_ptr<CompoundCurve>(cloneImpl());
+}
+
+CompoundCurve*
+CompoundCurve::cloneImpl() const
+{
+    return new CompoundCurve(*this);
+}
+
+std::unique_ptr<CompoundCurve>
+CompoundCurve::reverse() const
+{
+    return std::unique_ptr<CompoundCurve>(reverseImpl());
+}
+
+CompoundCurve*
+CompoundCurve::reverseImpl() const
+{
+    std::vector<std::unique_ptr<SimpleCurve>> reversed(curves.size());
+    std::transform(curves.rbegin(), curves.rend(), reversed.end(), [](const auto& curve) {
+        return std::unique_ptr<SimpleCurve>(static_cast<SimpleCurve*>(curve->reverse().release()));
+    });
+
+    return getFactory()->createCompoundCurve(std::move(reversed)).release();
+}
+
+Envelope
+CompoundCurve::computeEnvelopeInternal()
+{
+    Envelope e;
+    for (const auto& curve : curves) {
+        e.expandToInclude(curve->getEnvelopeInternal());
+    }
+    return e;
+}
+
+int
+CompoundCurve::compareToSameClass(const Geometry* g) const
+{
+    const CompoundCurve* curve = detail::down_cast<const CompoundCurve*>(g);
+    return compare(curves, curve->curves);
+}
+
+void
+CompoundCurve::normalize()
+{
+    throw std::runtime_error("Not implemented.");
+}
+
+void
+CompoundCurve::apply_ro(GeometryFilter*) const
+{
+    throw std::runtime_error("Not implemented.");
+}
+
+void
+CompoundCurve::apply_ro(GeometryComponentFilter*) const
+{
+    throw std::runtime_error("Not implemented.");
+}
+
+void
+CompoundCurve::apply_ro(CoordinateFilter*) const
+{
+    throw std::runtime_error("Not implemented.");
+}
+
+void
+CompoundCurve::apply_ro(CoordinateSequenceFilter&) const
+{
+    throw std::runtime_error("Not implemented.");
+}
+
+void
+CompoundCurve::apply_rw(GeometryFilter*)
+{
+    throw std::runtime_error("Not implemented.");
+}
+
+void
+CompoundCurve::apply_rw(GeometryComponentFilter*)
+{
+    throw std::runtime_error("Not implemented.");
+}
+
+void
+CompoundCurve::apply_rw(const CoordinateFilter*)
+{
+    throw std::runtime_error("Not implemented.");
+}
+
+void
+CompoundCurve::apply_rw(CoordinateSequenceFilter&)
+{
+    throw std::runtime_error("Not implemented.");
+}
+
+
+}
+}
diff --git a/src/geom/CurvePolygon.cpp b/src/geom/CurvePolygon.cpp
new file mode 100644
index 000000000..bf542dd52
--- /dev/null
+++ b/src/geom/CurvePolygon.cpp
@@ -0,0 +1,63 @@
+/**********************************************************************
+ *
+ * GEOS - Geometry Engine Open Source
+ * http://geos.osgeo.org
+ *
+ * Copyright (C) 2024 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/geom/Curve.h>
+#include <geos/geom/CurvePolygon.h>
+#include <geos/geom/CoordinateSequence.h>
+
+namespace geos {
+namespace geom {
+
+    std::unique_ptr<CoordinateSequence>
+    CurvePolygon::getCoordinates() const
+    {
+        auto coordinates = shell->getCoordinates();
+        for (const auto& hole : holes) {
+            // FIXME remove unncessary copy
+            coordinates->add(*hole->getCoordinates());
+        }
+        return coordinates;
+    }
+
+
+    std::string CurvePolygon::getGeometryType() const {
+        return "CurvePolygon";
+    }
+
+    GeometryTypeId CurvePolygon::getGeometryTypeId() const {
+        return GEOS_CURVEPOLYGON;
+    }
+
+    std::unique_ptr<Geometry>
+    CurvePolygon::getBoundary() const {
+        throw std::runtime_error("Not implemented.");
+    }
+
+    void
+    CurvePolygon::normalize() {
+        throw std::runtime_error("Not implemented.");
+    }
+
+    Geometry*
+    CurvePolygon::cloneImpl() const {
+        throw std::runtime_error("Not implemented.");
+    }
+
+    Geometry*
+    CurvePolygon::reverseImpl() const {
+        throw std::runtime_error("Not implemented.");
+    }
+
+}
+}
diff --git a/src/geom/Geometry.cpp b/src/geom/Geometry.cpp
index dfd37113e..eb15b0149 100644
--- a/src/geom/Geometry.cpp
+++ b/src/geom/Geometry.cpp
@@ -689,78 +689,6 @@ Geometry::GeometryChangedFilter::filter_rw(Geometry* geom)
     geom->geometryChangedAction();
 }
 
-int
-Geometry::compare(std::vector<Coordinate> a, std::vector<Coordinate> b) const
-{
-    std::size_t i = 0;
-    std::size_t j = 0;
-    while(i < a.size() && j < b.size()) {
-        Coordinate& aCoord = a[i];
-        Coordinate& bCoord = b[j];
-        int comparison = aCoord.compareTo(bCoord);
-        if(comparison != 0) {
-            return comparison;
-        }
-        i++;
-        j++;
-    }
-    if(i < a.size()) {
-        return 1;
-    }
-    if(j < b.size()) {
-        return -1;
-    }
-    return 0;
-}
-
-int
-Geometry::compare(std::vector<Geometry*> a, std::vector<Geometry*> b) const
-{
-    std::size_t i = 0;
-    std::size_t j = 0;
-    while(i < a.size() && j < b.size()) {
-        Geometry* aGeom = a[i];
-        Geometry* bGeom = b[j];
-        int comparison = aGeom->compareTo(bGeom);
-        if(comparison != 0) {
-            return comparison;
-        }
-        i++;
-        j++;
-    }
-    if(i < a.size()) {
-        return 1;
-    }
-    if(j < b.size()) {
-        return -1;
-    }
-    return 0;
-}
-
-int
-Geometry::compare(const std::vector<std::unique_ptr<Geometry>> & a,
-        const std::vector<std::unique_ptr<Geometry>> & b) const
-{
-    std::size_t i = 0;
-    std::size_t j = 0;
-    while(i < a.size() && j < b.size()) {
-        Geometry* aGeom = a[i].get();
-        Geometry* bGeom = b[j].get();
-        int comparison = aGeom->compareTo(bGeom);
-        if(comparison != 0) {
-            return comparison;
-        }
-        i++;
-        j++;
-    }
-    if(i < a.size()) {
-        return 1;
-    }
-    if(j < b.size()) {
-        return -1;
-    }
-    return 0;
-}
 
 /**
  *  Returns the minimum distance between this Geometry
diff --git a/src/geom/GeometryFactory.cpp b/src/geom/GeometryFactory.cpp
index 77a8a8995..a34e7ddd2 100644
--- a/src/geom/GeometryFactory.cpp
+++ b/src/geom/GeometryFactory.cpp
@@ -20,14 +20,19 @@
 
 #include <geos/geom/Coordinate.h>
 #include <geos/geom/CoordinateSequence.h>
+#include <geos/geom/CircularString.h>
+#include <geos/geom/CurvePolygon.h>
+#include <geos/geom/CompoundCurve.h>
 #include <geos/geom/GeometryFactory.h>
 #include <geos/geom/Point.h>
 #include <geos/geom/LineString.h>
 #include <geos/geom/LinearRing.h>
 #include <geos/geom/Polygon.h>
+#include <geos/geom/MultiCurve.h>
 #include <geos/geom/MultiPoint.h>
 #include <geos/geom/MultiLineString.h>
 #include <geos/geom/MultiPolygon.h>
+#include <geos/geom/MultiSurface.h>
 #include <geos/geom/GeometryCollection.h>
 #include <geos/geom/PrecisionModel.h>
 #include <geos/geom/Envelope.h>
@@ -303,6 +308,23 @@ GeometryFactory::createMultiLineString(std::vector<std::unique_ptr<Geometry>> &&
     return std::unique_ptr<MultiLineString>(new MultiLineString(std::move(fromLines), *this));
 }
 
+std::unique_ptr<MultiCurve>
+GeometryFactory::createMultiCurve() const {
+    return createMultiCurve(std::vector<std::unique_ptr<Curve>>());
+}
+
+std::unique_ptr<MultiCurve>
+GeometryFactory::createMultiCurve(std::vector<std::unique_ptr<Curve>> && fromCurves) const {
+    // Can't use make_unique because constructor is protected
+    return std::unique_ptr<MultiCurve>(new MultiCurve(std::move(fromCurves), *this));
+}
+
+std::unique_ptr<MultiCurve>
+GeometryFactory::createMultiCurve(std::vector<std::unique_ptr<Geometry>> && fromCurves) const {
+    // Can't use make_unique because constructor is protected
+    return std::unique_ptr<MultiCurve>(new MultiCurve(std::move(fromCurves), *this));
+}
+
 /*public*/
 std::unique_ptr<GeometryCollection>
 GeometryFactory::createGeometryCollection() const
@@ -365,6 +387,27 @@ GeometryFactory::createMultiPolygon(const std::vector<const Geometry*>& fromPoly
     return createMultiPolygon(std::move(newGeoms));
 }
 
+std::unique_ptr<MultiSurface>
+GeometryFactory::createMultiSurface() const
+{
+    // Can't use make_unique because constructor is protected
+    return std::unique_ptr<MultiSurface>(new MultiSurface(std::vector<std::unique_ptr<Surface>>(), *this));
+}
+
+std::unique_ptr<MultiSurface>
+GeometryFactory::createMultiSurface(std::vector<std::unique_ptr<Geometry>> && newSurfaces) const
+{
+    // Can't use make_unique because constructor is protected
+    return std::unique_ptr<MultiSurface>(new MultiSurface(std::move(newSurfaces), *this));
+}
+
+std::unique_ptr<MultiSurface>
+GeometryFactory::createMultiSurface(std::vector<std::unique_ptr<Surface>> && newSurfaces) const
+{
+    // Can't use make_unique because constructor is protected
+    return std::unique_ptr<MultiSurface>(new MultiSurface(std::move(newSurfaces), *this));
+}
+
 /*public*/
 std::unique_ptr<LinearRing>
 GeometryFactory::createLinearRing(std::size_t coordinateDimension) const
@@ -488,6 +531,25 @@ const
     return new Polygon(std::move(newRing), std::move(newHoles), *this);
 }
 
+/* public */
+std::unique_ptr<CurvePolygon>
+GeometryFactory::createCurvePolygon(std::unique_ptr<Curve> && shell)
+const
+{
+    //auto shellCurve = std::unique_ptr<Curve>(detail::down_cast<Curve*>(shell.release()));
+    // Can't use make_unique with protected constructor
+    return std::unique_ptr<CurvePolygon>(new CurvePolygon(std::move(shell), *this));
+}
+
+/* public */
+std::unique_ptr<CurvePolygon>
+GeometryFactory::createCurvePolygon(std::unique_ptr<Curve> && shell, std::vector<std::unique_ptr<Curve>> && holes)
+const
+{
+    // Can't use make_unique with protected constructor
+    return std::unique_ptr<CurvePolygon>(new CurvePolygon(std::move(shell), std::move(holes), *this));
+}
+
 /*public*/
 std::unique_ptr<LineString>
 GeometryFactory::createLineString(std::size_t coordinateDimension) const
@@ -496,6 +558,14 @@ GeometryFactory::createLineString(std::size_t coordinateDimension) const
     return createLineString(std::move(cs));
 }
 
+/*public*/
+std::unique_ptr<CircularString>
+GeometryFactory::createCircularString(bool hasZ, bool hasM) const
+{
+    auto cs = detail::make_unique<CoordinateSequence>(0u, hasZ, hasM);
+    return createCircularString(std::move(cs));
+}
+
 /*public*/
 std::unique_ptr<LineString>
 GeometryFactory::createLineString(const LineString& ls) const
@@ -504,6 +574,14 @@ GeometryFactory::createLineString(const LineString& ls) const
     return std::unique_ptr<LineString>(new LineString(ls));
 }
 
+/*public*/
+std::unique_ptr<CircularString>
+GeometryFactory::createCircularString(const CircularString& ls) const
+{
+    // Can't use make_unique with protected constructor
+    return std::unique_ptr<CircularString>(new CircularString(ls));
+}
+
 /*public*/
 std::unique_ptr<LineString>
 GeometryFactory::createLineString(CoordinateSequence::Ptr && newCoords)
@@ -515,6 +593,33 @@ const
     return std::unique_ptr<LineString>(new LineString(std::move(newCoords), *this));
 }
 
+/*public*/
+std::unique_ptr<CircularString>
+GeometryFactory::createCircularString(CoordinateSequence::Ptr && newCoords)
+const
+{
+    if (!newCoords)
+        return createCircularString();
+    // Can't use make_unique with protected constructor
+    return std::unique_ptr<CircularString>(new CircularString(std::move(newCoords), *this));
+}
+
+/*public*/
+std::unique_ptr<CompoundCurve>
+GeometryFactory::createCompoundCurve()
+const
+{
+    return std::unique_ptr<CompoundCurve>();
+}
+
+/*public*/
+std::unique_ptr<CompoundCurve>
+GeometryFactory::createCompoundCurve(std::vector<std::unique_ptr<SimpleCurve>>&& curves)
+const
+{
+    return std::unique_ptr<CompoundCurve>(new CompoundCurve(std::move(curves), *this));
+}
+
 /*public*/
 std::unique_ptr<LineString>
 GeometryFactory::createLineString(const CoordinateSequence& fromCoords)
@@ -524,6 +629,15 @@ const
     return std::unique_ptr<LineString>(new LineString(fromCoords.clone(), *this));
 }
 
+/*public*/
+std::unique_ptr<CircularString>
+GeometryFactory::createCircularString(const CoordinateSequence& fromCoords)
+const
+{
+    // Can't use make_unique with protected constructor
+    return std::unique_ptr<CircularString>(new CircularString(fromCoords.clone(), *this));
+}
+
 /*public*/
 std::unique_ptr<Geometry>
 GeometryFactory::createEmpty(int dimension) const
diff --git a/src/geom/LineString.cpp b/src/geom/LineString.cpp
index e3c84c3dc..f3a97822b 100644
--- a/src/geom/LineString.cpp
+++ b/src/geom/LineString.cpp
@@ -50,10 +50,7 @@ LineString::~LineString(){}
 
 /*protected*/
 LineString::LineString(const LineString& ls)
-    :
-    Geometry(ls),
-    points(ls.points->clone()),
-    envelope(ls.envelope)
+    : SimpleCurve(ls)
 {
 }
 
@@ -61,9 +58,7 @@ LineString::LineString(const LineString& ls)
 LineString::LineString(CoordinateSequence::Ptr && newCoords,
                        const GeometryFactory& factory)
     :
-    Geometry(&factory),
-    points(newCoords ? std::move(newCoords) : detail::make_unique<CoordinateSequence>()),
-    envelope(computeEnvelopeInternal())
+    SimpleCurve(std::move(newCoords), factory)
 {
     validateConstruction();
 }
@@ -88,7 +83,7 @@ void
 LineString::validateConstruction()
 {
     if(points.get() == nullptr) {
-        points = detail::make_unique<CoordinateSequence>();
+        points = std::make_unique<CoordinateSequence>();
         return;
     }
 
@@ -97,126 +92,6 @@ LineString::validateConstruction()
     }
 }
 
-std::unique_ptr<CoordinateSequence>
-LineString::getCoordinates() const
-{
-    assert(points.get());
-    return points->clone();
-    //return points;
-}
-
-const CoordinateSequence*
-LineString::getCoordinatesRO() const
-{
-    assert(nullptr != points.get());
-    return points.get();
-}
-
-std::unique_ptr<CoordinateSequence>
-LineString::releaseCoordinates()
-{
-    auto newPts = detail::make_unique<CoordinateSequence>(0u, points->hasZ(), points->hasM());
-    auto ret = std::move(points);
-    points = std::move(newPts);
-    geometryChanged();
-    return ret;
-}
-
-const Coordinate&
-LineString::getCoordinateN(std::size_t n) const
-{
-    assert(points.get());
-    return points->getAt(n);
-}
-
-Dimension::DimensionType
-LineString::getDimension() const
-{
-    return Dimension::L; // line
-}
-
-uint8_t
-LineString::getCoordinateDimension() const
-{
-    return (uint8_t) points->getDimension();
-}
-
-bool
-LineString::hasM() const
-{
-    return points->hasM();
-}
-
-bool
-LineString::hasZ() const
-{
-    return points->hasZ();
-}
-
-int
-LineString::getBoundaryDimension() const
-{
-    if(isClosed()) {
-        return Dimension::False;
-    }
-    return 0;
-}
-
-bool
-LineString::isEmpty() const
-{
-    assert(points.get());
-    return points->isEmpty();
-}
-
-std::size_t
-LineString::getNumPoints() const
-{
-    assert(points.get());
-    return points->getSize();
-}
-
-std::unique_ptr<Point>
-LineString::getPointN(std::size_t n) const
-{
-    assert(getFactory());
-    assert(points.get());
-    return std::unique_ptr<Point>(getFactory()->createPoint(points->getAt(n)));
-}
-
-std::unique_ptr<Point>
-LineString::getStartPoint() const
-{
-    if(isEmpty()) {
-        return nullptr;
-    }
-    return getPointN(0);
-}
-
-std::unique_ptr<Point>
-LineString::getEndPoint() const
-{
-    if(isEmpty()) {
-        return nullptr;
-    }
-    return getPointN(getNumPoints() - 1);
-}
-
-bool
-LineString::isClosed() const
-{
-    if(isEmpty()) {
-        return false;
-    }
-
-    return points->front<CoordinateXY>().equals2D(points->back<CoordinateXY>());
-}
-
-bool
-LineString::isRing() const
-{
-    return isClosed() && isSimple();
-}
 
 std::string
 LineString::getGeometryType() const
@@ -224,182 +99,6 @@ LineString::getGeometryType() const
     return "LineString";
 }
 
-std::unique_ptr<Geometry>
-LineString::getBoundary() const
-{
-    operation::BoundaryOp bop(*this);
-    return bop.getBoundary();
-}
-
-bool
-LineString::isCoordinate(Coordinate& pt) const
-{
-    assert(points.get());
-    std::size_t npts = points->getSize();
-    for(std::size_t i = 0; i < npts; i++) {
-        if(points->getAt<CoordinateXY>(i) == pt) {
-            return true;
-        }
-    }
-    return false;
-}
-
-/*protected*/
-Envelope
-LineString::computeEnvelopeInternal() const
-{
-    if(isEmpty()) {
-        return Envelope();
-    }
-
-    return points->getEnvelope();
-}
-
-bool
-LineString::equalsExact(const Geometry* other, double tolerance) const
-{
-    if(!isEquivalentClass(other)) {
-        return false;
-    }
-
-    const LineString* otherLineString = detail::down_cast<const LineString*>(other);
-    std::size_t npts = points->getSize();
-    if(npts != otherLineString->points->getSize()) {
-        return false;
-    }
-    for(std::size_t i = 0; i < npts; ++i) {
-        if(!equal(points->getAt<CoordinateXY>(i), otherLineString->points->getAt<CoordinateXY>(i), tolerance)) {
-            return false;
-        }
-    }
-    return true;
-}
-
-bool
-LineString::equalsIdentical(const Geometry* other_g) const
-{
-    if(!isEquivalentClass(other_g)) {
-        return false;
-    }
-
-    const auto& other = static_cast<const LineString&>(*other_g);
-
-    if (envelope != other.envelope) {
-        return false;
-    }
-
-    return getCoordinatesRO()->equalsIdentical(*other.getCoordinatesRO());
-}
-
-void
-LineString::apply_rw(const CoordinateFilter* filter)
-{
-    assert(points.get());
-    points->apply_rw(filter);
-}
-
-void
-LineString::apply_ro(CoordinateFilter* filter) const
-{
-    assert(points.get());
-    points->apply_ro(filter);
-}
-
-void
-LineString::apply_rw(GeometryFilter* filter)
-{
-    assert(filter);
-    filter->filter_rw(this);
-}
-
-void
-LineString::apply_ro(GeometryFilter* filter) const
-{
-    assert(filter);
-    filter->filter_ro(this);
-}
-
-/*private*/
-void
-LineString::normalizeClosed()
-{
-    if(isEmpty()) {
-        return;
-    }
-
-    const auto& ringCoords = getCoordinatesRO();
-
-    auto coords = detail::make_unique<CoordinateSequence>(0u, ringCoords->hasZ(), ringCoords->hasM());
-    coords->reserve(ringCoords->size());
-    // exclude last point (repeated)
-    coords->add(*ringCoords, 0, ringCoords->size() - 2);
-
-    const CoordinateXY* minCoordinate = coords->minCoordinate();
-
-    CoordinateSequence::scroll(coords.get(), minCoordinate);
-    coords->closeRing(true);
-
-    if(coords->size() >= 4 && algorithm::Orientation::isCCW(coords.get())) {
-        coords->reverse();
-    }
-
-    points = std::move(coords);
-}
-
-/*public*/
-void
-LineString::normalize()
-{
-    if (isEmpty()) return;
-    assert(points.get());
-    if (isClosed()) {
-        normalizeClosed();
-        return;
-    }
-    std::size_t npts = points->getSize();
-    std::size_t n = npts / 2;
-    for(std::size_t i = 0; i < n; i++) {
-        std::size_t j = npts - 1 - i;
-        if(!(points->getAt<CoordinateXY>(i) == points->getAt<CoordinateXY>(j))) {
-            if(points->getAt<CoordinateXY>(i).compareTo(points->getAt<CoordinateXY>(j)) > 0) {
-                points->reverse();
-            }
-            return;
-        }
-    }
-}
-
-int
-LineString::compareToSameClass(const Geometry* ls) const
-{
-    const LineString* line = detail::down_cast<const LineString*>(ls);
-
-    // MD - optimized implementation
-    std::size_t mynpts = points->getSize();
-    std::size_t othnpts = line->points->getSize();
-    if(mynpts > othnpts) {
-        return 1;
-    }
-    if(mynpts < othnpts) {
-        return -1;
-    }
-    for(std::size_t i = 0; i < mynpts; i++) {
-        int cmp = points->getAt<CoordinateXY>(i).compareTo(line->points->getAt<CoordinateXY>(i));
-        if(cmp) {
-            return cmp;
-        }
-    }
-    return 0;
-}
-
-const CoordinateXY*
-LineString::getCoordinate() const
-{
-    if(isEmpty()) {
-        return nullptr;
-    }
-    return &(points->getAt<CoordinateXY>(0));
-}
 
 double
 LineString::getLength() const
@@ -407,53 +106,6 @@ LineString::getLength() const
     return Length::ofLine(points.get());
 }
 
-void
-LineString::apply_rw(GeometryComponentFilter* filter)
-{
-    assert(filter);
-    filter->filter_rw(this);
-}
-
-void
-LineString::apply_ro(GeometryComponentFilter* filter) const
-{
-    assert(filter);
-    filter->filter_ro(this);
-}
-
-void
-LineString::apply_rw(CoordinateSequenceFilter& filter)
-{
-    std::size_t npts = points->size();
-    if(!npts) {
-        return;
-    }
-    for(std::size_t i = 0; i < npts; ++i) {
-        filter.filter_rw(*points, i);
-        if(filter.isDone()) {
-            break;
-        }
-    }
-    if(filter.isGeometryChanged()) {
-        geometryChanged();
-    }
-}
-
-void
-LineString::apply_ro(CoordinateSequenceFilter& filter) const
-{
-    std::size_t npts = points->size();
-    if(!npts) {
-        return;
-    }
-    for(std::size_t i = 0; i < npts; ++i) {
-        filter.filter_ro(*points, i);
-        if(filter.isDone()) {
-            break;
-        }
-    }
-    //if (filter.isGeometryChanged()) geometryChanged();
-}
 
 GeometryTypeId
 LineString::getGeometryTypeId() const
diff --git a/src/geom/MultiCurve.cpp b/src/geom/MultiCurve.cpp
new file mode 100644
index 000000000..9b4375094
--- /dev/null
+++ b/src/geom/MultiCurve.cpp
@@ -0,0 +1,112 @@
+/**********************************************************************
+ *
+ * GEOS - Geometry Engine Open Source
+ * http://geos.osgeo.org
+ *
+ * Copyright (C) 2001-2002 Vivid Solutions Inc.
+ * Copyright (C) 2005 2006 Refractions Research Inc.
+ * Copyright (C) 2024 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/geom/MultiCurve.h>
+#include <geos/util.h>
+#include <geos/operation/BoundaryOp.h>
+#include <geos/geom/GeometryFactory.h>
+
+namespace geos {
+namespace geom {
+
+/*protected*/
+MultiCurve::MultiCurve(std::vector<std::unique_ptr<Curve>>&& newLines,
+                       const GeometryFactory& factory)
+    : GeometryCollection(std::move(newLines), factory)
+{}
+
+MultiCurve::MultiCurve(std::vector<std::unique_ptr<Geometry>>&& newLines,
+                       const GeometryFactory& factory)
+    : GeometryCollection(std::move(newLines), factory)
+{}
+
+Dimension::DimensionType
+MultiCurve::getDimension() const
+{
+    return Dimension::L; // line
+}
+
+int
+MultiCurve::getBoundaryDimension() const
+{
+    if (isClosed()) {
+        return Dimension::False;
+    }
+    return 0;
+}
+
+std::string
+MultiCurve::getGeometryType() const
+{
+    return "MultiCurve";
+}
+
+bool
+MultiCurve::isClosed() const
+{
+    if (isEmpty()) {
+        return false;
+    }
+    for (const auto& g : geometries) {
+        const Curve* ls = detail::down_cast<const Curve*>(g.get());
+        if (! ls->isClosed()) {
+            return false;
+        }
+    }
+    return true;
+}
+
+std::unique_ptr<Geometry>
+MultiCurve::getBoundary() const
+{
+    operation::BoundaryOp bop(*this);
+    return bop.getBoundary();
+}
+
+GeometryTypeId
+MultiCurve::getGeometryTypeId() const
+{
+    return GEOS_MULTICURVE;
+}
+
+MultiCurve*
+MultiCurve::reverseImpl() const
+{
+    if (isEmpty()) {
+        return clone().release();
+    }
+
+    std::vector<std::unique_ptr<Geometry>> reversed(geometries.size());
+
+    std::transform(geometries.begin(),
+                   geometries.end(),
+                   reversed.begin(),
+    [](const std::unique_ptr<Geometry>& g) {
+        return g->reverse();
+    });
+
+    return getFactory()->createMultiCurve(std::move(reversed)).release();
+}
+
+const Curve*
+MultiCurve::getGeometryN(std::size_t i) const
+{
+    return static_cast<const Curve*>(geometries[i].get());
+}
+
+
+}
+}
diff --git a/src/geom/MultiSurface.cpp b/src/geom/MultiSurface.cpp
new file mode 100644
index 000000000..82cc4c572
--- /dev/null
+++ b/src/geom/MultiSurface.cpp
@@ -0,0 +1,111 @@
+/**********************************************************************
+ *
+ * GEOS - Geometry Engine Open Source
+ * http://geos.osgeo.org
+ *
+ * Copyright (C) 2001-2002 Vivid Solutions Inc.
+ * Copyright (C) 2005 2006 Refractions Research Inc.
+ * Copyright (C) 2024 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/geom/MultiSurface.h>
+#include <geos/geom/MultiCurve.h>
+#include <geos/geom/GeometryFactory.h>
+
+namespace geos {
+namespace geom {
+
+/*protected*/
+MultiSurface::MultiSurface(std::vector<std::unique_ptr<Geometry>>&& newPolys, const GeometryFactory& factory)
+    : GeometryCollection(std::move(newPolys), factory)
+{
+    // FIXME check that all elements are in fact surfaces
+}
+
+MultiSurface::MultiSurface(std::vector<std::unique_ptr<Surface>>&& newPolys, const GeometryFactory& factory)
+    : GeometryCollection(std::move(newPolys), factory)
+{
+}
+
+MultiSurface::~MultiSurface() {}
+
+Dimension::DimensionType
+MultiSurface::getDimension() const
+{
+    return Dimension::A; // area
+}
+
+int
+MultiSurface::getBoundaryDimension() const
+{
+    return 1;
+}
+
+std::string
+MultiSurface::getGeometryType() const
+{
+    return "MultiSurface";
+}
+
+std::unique_ptr<Geometry>
+MultiSurface::getBoundary() const
+{
+    // FIXME implement
+#if 0
+    if (isEmpty()) {
+        return std::unique_ptr<Geometry>(getFactory()->createMultiCurve());
+    }
+#endif
+
+    std::vector<std::unique_ptr<Geometry>> allRings;
+    for (const auto& pg : geometries) {
+        auto g = pg->getBoundary();
+
+        if (g->getNumGeometries() == 1) {
+            allRings.push_back(std::move(g));
+        }
+        else {
+            for (std::size_t i = 0; i < g->getNumGeometries(); ++i) {
+                // TODO avoid this clone
+                allRings.push_back(g->getGeometryN(i)->clone());
+            }
+        }
+    }
+
+    return getFactory()->createMultiCurve(std::move(allRings));
+}
+
+GeometryTypeId
+MultiSurface::getGeometryTypeId() const
+{
+    return GEOS_MULTISURFACE;
+}
+
+MultiSurface*
+MultiSurface::reverseImpl() const
+{
+    if (isEmpty()) {
+        return clone().release();
+    }
+
+    std::vector<std::unique_ptr<Geometry>> reversed(geometries.size());
+
+    std::transform(geometries.begin(),
+                   geometries.end(),
+                   reversed.begin(),
+    [](const std::unique_ptr<Geometry>& g) {
+        return g->reverse();
+    });
+
+    return getFactory()->createMultiSurface(std::move(reversed)).release();
+}
+
+
+}
+}
diff --git a/src/geom/Polygon.cpp b/src/geom/Polygon.cpp
index 515275d51..938a1786d 100644
--- a/src/geom/Polygon.cpp
+++ b/src/geom/Polygon.cpp
@@ -28,9 +28,6 @@
 #include <geos/geom/GeometryFactory.h>
 #include <geos/geom/Dimension.h>
 #include <geos/geom/Envelope.h>
-#include <geos/geom/CoordinateSequenceFilter.h>
-#include <geos/geom/GeometryFilter.h>
-#include <geos/geom/GeometryComponentFilter.h>
 #include <geos/util.h>
 
 #include <vector>
@@ -48,57 +45,14 @@
 namespace geos {
 namespace geom { // geos::geom
 
-/*protected*/
-Polygon::Polygon(const Polygon& p)
-    :
-    Geometry(p),
-    shell(detail::make_unique<LinearRing>(*p.shell)),
-    holes(p.holes.size())
-{
-    for(std::size_t i = 0; i < holes.size(); ++i) {
-        holes[i] = detail::make_unique<LinearRing>(*p.holes[i]);
-    }
-}
-
-Polygon::Polygon(std::unique_ptr<LinearRing> && newShell,
-                 const GeometryFactory& newFactory) :
-        Geometry(&newFactory),
-        shell(std::move(newShell))
-{
-    if(shell == nullptr) {
-        shell = getFactory()->createLinearRing();
-    }
-}
-
-Polygon::Polygon(std::unique_ptr<LinearRing> && newShell,
-                 std::vector<std::unique_ptr<LinearRing>> && newHoles,
-                 const GeometryFactory& newFactory) :
-                 Geometry(&newFactory),
-                 shell(std::move(newShell)),
-                 holes(std::move(newHoles))
-{
-    if(shell == nullptr) {
-        shell = getFactory()->createLinearRing();
-    }
-
-    // TODO move into validateConstruction() method
-    if(shell->isEmpty() && hasNonEmptyElements(&holes)) {
-        throw util::IllegalArgumentException("shell is empty but holes are not");
-    }
-    if (hasNullElements(&holes)) {
-        throw util::IllegalArgumentException("holes must not contain null elements");
-    }
-}
-
-
 std::unique_ptr<CoordinateSequence>
 Polygon::getCoordinates() const
 {
     if(isEmpty()) {
-        return detail::make_unique<CoordinateSequence>(0u, hasZ(), hasM());
+        return std::make_unique<CoordinateSequence>(0u, hasZ(), hasM());
     }
 
-    auto cl = detail::make_unique<CoordinateSequence>(0u, hasZ(), hasM());
+    auto cl = std::make_unique<CoordinateSequence>(0u, hasZ(), hasM());
     cl->reserve(getNumPoints());
 
     // Add shell points
@@ -112,110 +66,6 @@ Polygon::getCoordinates() const
     return cl;
 }
 
-size_t
-Polygon::getNumPoints() const
-{
-    std::size_t numPoints = shell->getNumPoints();
-    for(const auto& lr : holes) {
-        numPoints += lr->getNumPoints();
-    }
-    return numPoints;
-}
-
-Dimension::DimensionType
-Polygon::getDimension() const
-{
-    return Dimension::A; // area
-}
-
-uint8_t
-Polygon::getCoordinateDimension() const
-{
-    uint8_t dimension = 2;
-
-    if(shell != nullptr) {
-        dimension = std::max(dimension, shell->getCoordinateDimension());
-    }
-
-    for(const auto& hole : holes) {
-        dimension = std::max(dimension, hole->getCoordinateDimension());
-    }
-
-    return dimension;
-}
-
-bool
-Polygon::hasM() const {
-    if (shell->getCoordinatesRO()->hasM()) {
-        return true;
-    }
-
-    for (const auto& hole : holes) {
-        if (hole->getCoordinatesRO()->hasM()) {
-            return true;
-        }
-    }
-
-    return false;
-}
-
-bool
-Polygon::hasZ() const {
-    if (shell->getCoordinatesRO()->hasZ()) {
-        return true;
-    }
-
-    for (const auto& hole : holes) {
-        if (hole->getCoordinatesRO()->hasZ()) {
-            return true;
-        }
-    }
-
-    return false;
-}
-
-int
-Polygon::getBoundaryDimension() const
-{
-    return 1;
-}
-
-bool
-Polygon::isEmpty() const
-{
-    return shell->isEmpty();
-}
-
-const LinearRing*
-Polygon::getExteriorRing() const
-{
-    return shell.get();
-}
-
-std::unique_ptr<LinearRing>
-Polygon::releaseExteriorRing()
-{
-    return std::move(shell);
-}
-
-size_t
-Polygon::getNumInteriorRing() const
-{
-    return holes.size();
-}
-
-const LinearRing*
-Polygon::getInteriorRingN(std::size_t n) const
-{
-    return holes[n].get();
-}
-
-std::vector<std::unique_ptr<LinearRing>>
-Polygon::releaseInteriorRings()
-{
-    return std::move(holes);
-}
-
 std::string
 Polygon::getGeometryType() const
 {
@@ -255,151 +105,6 @@ Polygon::getBoundary() const
     return getFactory()->createMultiLineString(std::move(rings));
 }
 
-bool
-Polygon::equalsExact(const Geometry* other, double tolerance) const
-{
-    if(!isEquivalentClass(other)) {
-        return false;
-    }
-
-    const Polygon* otherPolygon = detail::down_cast<const Polygon*>(other);
-    if(! otherPolygon) {
-        return false;
-    }
-
-    if(!shell->equalsExact(otherPolygon->shell.get(), tolerance)) {
-        return false;
-    }
-
-    std::size_t nholes = holes.size();
-
-    if(nholes != otherPolygon->holes.size()) {
-        return false;
-    }
-
-    for(std::size_t i = 0; i < nholes; i++) {
-        const LinearRing* hole = holes[i].get();
-        const LinearRing* otherhole = otherPolygon->holes[i].get();
-        if(!hole->equalsExact(otherhole, tolerance)) {
-            return false;
-        }
-    }
-
-    return true;
-}
-
-bool
-Polygon::equalsIdentical(const Geometry* other_g) const
-{
-    if(!isEquivalentClass(other_g)) {
-        return false;
-    }
-
-    const auto& other = static_cast<const Polygon&>(*other_g);
-
-    if (getNumInteriorRing() != other.getNumInteriorRing()) {
-        return false;
-    }
-
-    if (!getExteriorRing()->equalsIdentical(other.getExteriorRing())) {
-            return false;
-    }
-
-    for (std::size_t i = 0; i < getNumInteriorRing(); i++) {
-        if (!getInteriorRingN(i)->equalsIdentical(other.getInteriorRingN(i))) {
-            return false;
-        }
-    }
-
-    return true;
-}
-
-void
-Polygon::apply_ro(CoordinateFilter* filter) const
-{
-    shell->apply_ro(filter);
-    for(const auto& lr : holes) {
-        lr->apply_ro(filter);
-    }
-}
-
-void
-Polygon::apply_rw(const CoordinateFilter* filter)
-{
-    shell->apply_rw(filter);
-    for(auto& lr : holes) {
-        lr->apply_rw(filter);
-    }
-}
-
-void
-Polygon::apply_rw(GeometryFilter* filter)
-{
-    filter->filter_rw(this);
-}
-
-void
-Polygon::apply_ro(GeometryFilter* filter) const
-{
-    filter->filter_ro(this);
-}
-
-std::unique_ptr<Geometry>
-Polygon::convexHull() const
-{
-    return getExteriorRing()->convexHull();
-}
-
-
-void
-Polygon::normalize()
-{
-    normalize(shell.get(), true);
-    for(auto& lr : holes) {
-        normalize(lr.get(), false);
-    }
-    std::sort(holes.begin(), holes.end(), [](const std::unique_ptr<LinearRing> & a, const std::unique_ptr<LinearRing> & b) {
-        return a->compareTo(b.get()) > 0;
-    });
-}
-
-void
-Polygon::orientRings(bool exteriorCW)
-{
-    shell->orient(exteriorCW);
-    for (auto& hole : holes) {
-        hole->orient(!exteriorCW);
-    }
-}
-
-int
-Polygon::compareToSameClass(const Geometry* g) const
-{
-    const Polygon* p = detail::down_cast<const Polygon*>(g);
-    int shellComp = shell->compareToSameClass(p->shell.get());
-    if (shellComp != 0) {
-        return shellComp;
-    }
-
-    size_t nHole1 = getNumInteriorRing();
-    size_t nHole2 = p->getNumInteriorRing();
-    if (nHole1 < nHole2) {
-        return -1;
-    }
-    if (nHole1 > nHole2) {
-        return 1;
-    }
-
-    for (size_t i=0; i < nHole1; i++) {
-        const LinearRing *lr = p->getInteriorRingN(i);
-        const int holeComp = getInteriorRingN(i)->compareToSameClass(lr);
-        if (holeComp != 0) {
-            return holeComp;
-        }
-    }
-
-    return 0;
-}
 
 /*
  * TODO: check this function, there should be CoordinateSequence copy
@@ -430,11 +135,6 @@ Polygon::normalize(LinearRing* ring, bool clockwise)
     ring->setPoints(&coords);
 }
 
-const CoordinateXY*
-Polygon::getCoordinate() const
-{
-    return shell->getCoordinate();
-}
 
 /*
  *  Returns the area of this <code>Polygon</code>
@@ -469,59 +169,6 @@ Polygon::getLength() const
     return len;
 }
 
-void
-Polygon::apply_ro(GeometryComponentFilter* filter) const
-{
-    filter->filter_ro(this);
-    shell->apply_ro(filter);
-    for(std::size_t i = 0, n = holes.size(); i < n && !filter->isDone(); ++i) {
-        holes[i]->apply_ro(filter);
-    }
-}
-
-void
-Polygon::apply_rw(GeometryComponentFilter* filter)
-{
-    filter->filter_rw(this);
-    shell->apply_rw(filter);
-    for(std::size_t i = 0, n = holes.size(); i < n && !filter->isDone(); ++i) {
-        holes[i]->apply_rw(filter);
-    }
-}
-
-void
-Polygon::apply_rw(CoordinateSequenceFilter& filter)
-{
-    shell->apply_rw(filter);
-
-    if(! filter.isDone()) {
-        for(std::size_t i = 0, n = holes.size(); i < n; ++i) {
-            holes[i]->apply_rw(filter);
-            if(filter.isDone()) {
-                break;
-            }
-        }
-    }
-    if(filter.isGeometryChanged()) {
-        geometryChanged();
-    }
-}
-
-void
-Polygon::apply_ro(CoordinateSequenceFilter& filter) const
-{
-    shell->apply_ro(filter);
-
-    if(! filter.isDone()) {
-        for(std::size_t i = 0, n = holes.size(); i < n; ++i) {
-            holes[i]->apply_ro(filter);
-            if(filter.isDone()) {
-                break;
-            }
-        }
-    }
-    //if (filter.isGeometryChanged()) geometryChanged();
-}
 
 GeometryTypeId
 Polygon::getGeometryTypeId() const
@@ -572,6 +219,16 @@ Polygon::isRectangle() const
     return true;
 }
 
+void
+Polygon::orientRings(bool exteriorCW)
+{
+    shell->orient(exteriorCW);
+    for (auto& hole : holes) {
+        hole->orient(!exteriorCW);
+    }
+}
+
+
 Polygon*
 Polygon::reverseImpl() const
 {
@@ -591,5 +248,17 @@ Polygon::reverseImpl() const
     return getFactory()->createPolygon(shell->reverse(), std::move(interiorRingsReversed)).release();
 }
 
+void
+Polygon::normalize()
+{
+    normalize(shell.get(), true);
+    for(auto& lr : holes) {
+        normalize(lr.get(), false);
+    }
+    std::sort(holes.begin(), holes.end(), [](const auto& a, const auto& b) {
+        return a->compareTo(b.get()) > 0;
+    });
+}
+
 } // namespace geos::geom
 } // namespace geos
diff --git a/src/geom/LineString.cpp b/src/geom/SimpleCurve.cpp
similarity index 60%
copy from src/geom/LineString.cpp
copy to src/geom/SimpleCurve.cpp
index e3c84c3dc..9fc75e1af 100644
--- a/src/geom/LineString.cpp
+++ b/src/geom/SimpleCurve.cpp
@@ -3,158 +3,100 @@
  * GEOS - Geometry Engine Open Source
  * http://geos.osgeo.org
  *
- * Copyright (C) 2011 Sandro Santilli <strk at kbt.io>
- * Copyright (C) 2005-2006 Refractions Research Inc.
  * Copyright (C) 2001-2002 Vivid Solutions Inc.
+ * Copyright (C) 2005 2006 Refractions Research Inc.
+ * Copyright (C) 2011 Sandro Santilli <strk at kbt.io>
+ * Copyright (C) 2024 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.
  *
- **********************************************************************
- *
- * Last port: geom/LineString.java r320 (JTS-1.12)
- *
  **********************************************************************/
 
-#include <geos/util/IllegalArgumentException.h>
-#include <geos/algorithm/Length.h>
+#include <geos/geom/SimpleCurve.h>
+
 #include <geos/algorithm/Orientation.h>
-#include <geos/geom/Coordinate.h>
+#include <geos/geom/CoordinateFilter.h>
+#include <geos/geom/GeometryFactory.h>
+#include <geos/geom/GeometryFilter.h>
+#include <geos/operation/BoundaryOp.h>
 #include <geos/geom/CoordinateSequence.h>
 #include <geos/geom/CoordinateSequenceFilter.h>
-#include <geos/geom/CoordinateFilter.h>
-#include <geos/geom/Dimension.h>
-#include <geos/geom/GeometryFilter.h>
-#include <geos/geom/GeometryComponentFilter.h>
-#include <geos/geom/GeometryFactory.h>
-#include <geos/geom/LineString.h>
-#include <geos/geom/Point.h>
-#include <geos/geom/MultiPoint.h> // for getBoundary
-#include <geos/geom/Envelope.h>
-#include <geos/operation/BoundaryOp.h>
 #include <geos/util.h>
 
-#include <algorithm>
-#include <typeinfo>
-#include <memory>
-#include <cassert>
-
-using namespace geos::algorithm;
 
 namespace geos {
-namespace geom { // geos::geom
+namespace geom {
 
-LineString::~LineString(){}
-
-/*protected*/
-LineString::LineString(const LineString& ls)
-    :
-    Geometry(ls),
-    points(ls.points->clone()),
-    envelope(ls.envelope)
+SimpleCurve::SimpleCurve(const SimpleCurve& other)
+    : Curve(other),
+      points(other.points->clone()),
+      envelope(other.envelope)
 {
 }
 
-/*public*/
-LineString::LineString(CoordinateSequence::Ptr && newCoords,
-                       const GeometryFactory& factory)
-    :
-    Geometry(&factory),
-    points(newCoords ? std::move(newCoords) : detail::make_unique<CoordinateSequence>()),
+SimpleCurve::SimpleCurve(std::unique_ptr<CoordinateSequence>&& newCoords,
+            const GeometryFactory& factory)
+    : Curve(factory),
+    points(newCoords ? std::move(newCoords) : std::make_unique<CoordinateSequence>()),
     envelope(computeEnvelopeInternal())
 {
-    validateConstruction();
-}
-
-LineString*
-LineString::reverseImpl() const
-{
-    if(isEmpty()) {
-        return clone().release();
-    }
-
-    assert(points.get());
-    auto seq = points->clone();
-    seq->reverse();
-    assert(getFactory());
-    return getFactory()->createLineString(std::move(seq)).release();
-}
-
-
-/*private*/
-void
-LineString::validateConstruction()
-{
-    if(points.get() == nullptr) {
-        points = detail::make_unique<CoordinateSequence>();
-        return;
-    }
-
-    if(points->size() == 1) {
-        throw util::IllegalArgumentException("point array must contain 0 or >1 elements\n");
-    }
 }
 
 std::unique_ptr<CoordinateSequence>
-LineString::getCoordinates() const
+SimpleCurve::getCoordinates() const
 {
     assert(points.get());
     return points->clone();
-    //return points;
 }
 
 const CoordinateSequence*
-LineString::getCoordinatesRO() const
+SimpleCurve::getCoordinatesRO() const
 {
     assert(nullptr != points.get());
     return points.get();
 }
 
 std::unique_ptr<CoordinateSequence>
-LineString::releaseCoordinates()
+SimpleCurve::releaseCoordinates()
 {
-    auto newPts = detail::make_unique<CoordinateSequence>(0u, points->hasZ(), points->hasM());
+    auto newPts = std::make_unique<CoordinateSequence>(0u, points->hasZ(), points->hasM());
     auto ret = std::move(points);
     points = std::move(newPts);
     geometryChanged();
     return ret;
 }
 
+
 const Coordinate&
-LineString::getCoordinateN(std::size_t n) const
+SimpleCurve::getCoordinateN(std::size_t n) const
 {
     assert(points.get());
     return points->getAt(n);
 }
 
-Dimension::DimensionType
-LineString::getDimension() const
-{
-    return Dimension::L; // line
-}
-
 uint8_t
-LineString::getCoordinateDimension() const
+SimpleCurve::getCoordinateDimension() const
 {
     return (uint8_t) points->getDimension();
 }
 
 bool
-LineString::hasM() const
+SimpleCurve::hasM() const
 {
     return points->hasM();
 }
 
 bool
-LineString::hasZ() const
+SimpleCurve::hasZ() const
 {
     return points->hasZ();
 }
 
 int
-LineString::getBoundaryDimension() const
+SimpleCurve::getBoundaryDimension() const
 {
     if(isClosed()) {
         return Dimension::False;
@@ -162,22 +104,23 @@ LineString::getBoundaryDimension() const
     return 0;
 }
 
+
 bool
-LineString::isEmpty() const
+SimpleCurve::isEmpty() const
 {
     assert(points.get());
     return points->isEmpty();
 }
 
 std::size_t
-LineString::getNumPoints() const
+SimpleCurve::getNumPoints() const
 {
     assert(points.get());
     return points->getSize();
 }
 
 std::unique_ptr<Point>
-LineString::getPointN(std::size_t n) const
+SimpleCurve::getPointN(std::size_t n) const
 {
     assert(getFactory());
     assert(points.get());
@@ -185,7 +128,7 @@ LineString::getPointN(std::size_t n) const
 }
 
 std::unique_ptr<Point>
-LineString::getStartPoint() const
+SimpleCurve::getStartPoint() const
 {
     if(isEmpty()) {
         return nullptr;
@@ -194,7 +137,7 @@ LineString::getStartPoint() const
 }
 
 std::unique_ptr<Point>
-LineString::getEndPoint() const
+SimpleCurve::getEndPoint() const
 {
     if(isEmpty()) {
         return nullptr;
@@ -203,7 +146,7 @@ LineString::getEndPoint() const
 }
 
 bool
-LineString::isClosed() const
+SimpleCurve::isClosed() const
 {
     if(isEmpty()) {
         return false;
@@ -213,26 +156,20 @@ LineString::isClosed() const
 }
 
 bool
-LineString::isRing() const
+SimpleCurve::isRing() const
 {
     return isClosed() && isSimple();
 }
 
-std::string
-LineString::getGeometryType() const
-{
-    return "LineString";
-}
-
 std::unique_ptr<Geometry>
-LineString::getBoundary() const
+SimpleCurve::getBoundary() const
 {
     operation::BoundaryOp bop(*this);
     return bop.getBoundary();
 }
 
 bool
-LineString::isCoordinate(Coordinate& pt) const
+SimpleCurve::isCoordinate(Coordinate& pt) const
 {
     assert(points.get());
     std::size_t npts = points->getSize();
@@ -246,7 +183,7 @@ LineString::isCoordinate(Coordinate& pt) const
 
 /*protected*/
 Envelope
-LineString::computeEnvelopeInternal() const
+SimpleCurve::computeEnvelopeInternal() const
 {
     if(isEmpty()) {
         return Envelope();
@@ -255,20 +192,29 @@ LineString::computeEnvelopeInternal() const
     return points->getEnvelope();
 }
 
+const CoordinateXY*
+SimpleCurve::getCoordinate() const
+{
+    if(isEmpty()) {
+        return nullptr;
+    }
+    return &(points->getAt<CoordinateXY>(0));
+}
+
 bool
-LineString::equalsExact(const Geometry* other, double tolerance) const
+SimpleCurve::equalsExact(const Geometry* other, double tolerance) const
 {
     if(!isEquivalentClass(other)) {
         return false;
     }
 
-    const LineString* otherLineString = detail::down_cast<const LineString*>(other);
+    const SimpleCurve* otherCurve = detail::down_cast<const SimpleCurve*>(other);
     std::size_t npts = points->getSize();
-    if(npts != otherLineString->points->getSize()) {
+    if(npts != otherCurve->points->getSize()) {
         return false;
     }
     for(std::size_t i = 0; i < npts; ++i) {
-        if(!equal(points->getAt<CoordinateXY>(i), otherLineString->points->getAt<CoordinateXY>(i), tolerance)) {
+        if(!equal(points->getAt<CoordinateXY>(i), otherCurve->points->getAt<CoordinateXY>(i), tolerance)) {
             return false;
         }
     }
@@ -276,13 +222,13 @@ LineString::equalsExact(const Geometry* other, double tolerance) const
 }
 
 bool
-LineString::equalsIdentical(const Geometry* other_g) const
+SimpleCurve::equalsIdentical(const Geometry* other_g) const
 {
     if(!isEquivalentClass(other_g)) {
         return false;
     }
 
-    const auto& other = static_cast<const LineString&>(*other_g);
+    const auto& other = static_cast<const SimpleCurve&>(*other_g);
 
     if (envelope != other.envelope) {
         return false;
@@ -291,37 +237,33 @@ LineString::equalsIdentical(const Geometry* other_g) const
     return getCoordinatesRO()->equalsIdentical(*other.getCoordinatesRO());
 }
 
-void
-LineString::apply_rw(const CoordinateFilter* filter)
+int
+SimpleCurve::compareToSameClass(const Geometry* ls) const
 {
-    assert(points.get());
-    points->apply_rw(filter);
+    const SimpleCurve* line = detail::down_cast<const SimpleCurve*>(ls);
+
+    // MD - optimized implementation
+    std::size_t mynpts = points->getSize();
+    std::size_t othnpts = line->points->getSize();
+    if(mynpts > othnpts) {
+        return 1;
+    }
+    if(mynpts < othnpts) {
+        return -1;
+    }
+    for(std::size_t i = 0; i < mynpts; i++) {
+        int cmp = points->getAt<CoordinateXY>(i).compareTo(line->points->getAt<CoordinateXY>(i));
+        if(cmp) {
+            return cmp;
+        }
+    }
+    return 0;
 }
 
-void
-LineString::apply_ro(CoordinateFilter* filter) const
-{
-    assert(points.get());
-    points->apply_ro(filter);
-}
-
-void
-LineString::apply_rw(GeometryFilter* filter)
-{
-    assert(filter);
-    filter->filter_rw(this);
-}
-
-void
-LineString::apply_ro(GeometryFilter* filter) const
-{
-    assert(filter);
-    filter->filter_ro(this);
-}
 
 /*private*/
 void
-LineString::normalizeClosed()
+SimpleCurve::normalizeClosed()
 {
     if(isEmpty()) {
         return;
@@ -346,9 +288,10 @@ LineString::normalizeClosed()
     points = std::move(coords);
 }
 
+
 /*public*/
 void
-LineString::normalize()
+SimpleCurve::normalize()
 {
     if (isEmpty()) return;
     assert(points.get());
@@ -369,60 +312,51 @@ LineString::normalize()
     }
 }
 
-int
-LineString::compareToSameClass(const Geometry* ls) const
-{
-    const LineString* line = detail::down_cast<const LineString*>(ls);
 
-    // MD - optimized implementation
-    std::size_t mynpts = points->getSize();
-    std::size_t othnpts = line->points->getSize();
-    if(mynpts > othnpts) {
-        return 1;
-    }
-    if(mynpts < othnpts) {
-        return -1;
-    }
-    for(std::size_t i = 0; i < mynpts; i++) {
-        int cmp = points->getAt<CoordinateXY>(i).compareTo(line->points->getAt<CoordinateXY>(i));
-        if(cmp) {
-            return cmp;
-        }
-    }
-    return 0;
-}
-
-const CoordinateXY*
-LineString::getCoordinate() const
+void
+SimpleCurve::apply_rw(const CoordinateFilter* filter)
 {
-    if(isEmpty()) {
-        return nullptr;
-    }
-    return &(points->getAt<CoordinateXY>(0));
-}
-
-double
-LineString::getLength() const
-{
-    return Length::ofLine(points.get());
+    assert(points.get());
+    points->apply_rw(filter);
 }
 
 void
-LineString::apply_rw(GeometryComponentFilter* filter)
+SimpleCurve::apply_ro(CoordinateFilter* filter) const
+{
+    assert(points.get());
+    points->apply_ro(filter);
+}
+
+void
+SimpleCurve::apply_rw(GeometryFilter* filter)
 {
     assert(filter);
     filter->filter_rw(this);
 }
 
 void
-LineString::apply_ro(GeometryComponentFilter* filter) const
+SimpleCurve::apply_ro(GeometryFilter* filter) const
 {
     assert(filter);
     filter->filter_ro(this);
 }
 
 void
-LineString::apply_rw(CoordinateSequenceFilter& filter)
+SimpleCurve::apply_rw(GeometryComponentFilter* filter)
+{
+    assert(filter);
+    filter->filter_rw(this);
+}
+
+void
+SimpleCurve::apply_ro(GeometryComponentFilter* filter) const
+{
+    assert(filter);
+    filter->filter_ro(this);
+}
+
+void
+SimpleCurve::apply_rw(CoordinateSequenceFilter& filter)
 {
     std::size_t npts = points->size();
     if(!npts) {
@@ -440,7 +374,7 @@ LineString::apply_rw(CoordinateSequenceFilter& filter)
 }
 
 void
-LineString::apply_ro(CoordinateSequenceFilter& filter) const
+SimpleCurve::apply_ro(CoordinateSequenceFilter& filter) const
 {
     std::size_t npts = points->size();
     if(!npts) {
@@ -455,11 +389,5 @@ LineString::apply_ro(CoordinateSequenceFilter& filter) const
     //if (filter.isGeometryChanged()) geometryChanged();
 }
 
-GeometryTypeId
-LineString::getGeometryTypeId() const
-{
-    return GEOS_LINESTRING;
-}
-
 } // namespace geos::geom
 } // namespace geos
diff --git a/src/geom/Surface.cpp b/src/geom/Surface.cpp
new file mode 100644
index 000000000..c3ec61346
--- /dev/null
+++ b/src/geom/Surface.cpp
@@ -0,0 +1,276 @@
+/**********************************************************************
+ *
+ * GEOS - Geometry Engine Open Source
+ * http://geos.osgeo.org
+ *
+ * Copyright (C) 2024 ISciences LLC
+ * Copyright (C) 2011 Sandro Santilli <strk at kbt.io>
+ * Copyright (C) 2005-2006 Refractions Research Inc.
+ * Copyright (C) 2001-2002 Vivid Solutions Inc.
+ *
+ * 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.
+ *
+ **********************************************************************
+ *
+ * Last port: geom/Polygon.java r320 (JTS-1.12)
+ *
+ **********************************************************************/
+
+#include <geos/geom/Curve.h>
+#include <geos/geom/GeometryFactory.h>
+#include <geos/geom/Surface.h>
+#include <geos/util.h>
+
+namespace geos {
+namespace geom {
+
+void
+Surface::apply_ro(CoordinateFilter* filter) const
+{
+    getExteriorRing()->apply_ro(filter);
+    for (std::size_t i = 0; i < getNumInteriorRing(); i++) {
+        getInteriorRingN(i)->apply_ro(filter);
+    }
+}
+
+void
+Surface::apply_rw(const CoordinateFilter* filter)
+{
+    getExteriorRing()->apply_rw(filter);
+    for (std::size_t i = 0; i < getNumInteriorRing(); i++) {
+        getInteriorRingN(i)->apply_rw(filter);
+    }
+}
+
+void
+Surface::apply_rw(GeometryFilter* filter)
+{
+    filter->filter_rw(this);
+}
+
+void
+Surface::apply_ro(GeometryFilter* filter) const
+{
+    filter->filter_ro(this);
+}
+
+void
+Surface::apply_ro(GeometryComponentFilter* filter) const
+{
+    filter->filter_ro(this);
+    getExteriorRing()->apply_ro(filter);
+    for (std::size_t i = 0; !filter->isDone() && i < getNumInteriorRing(); i++) {
+        getInteriorRingN(i)->apply_ro(filter);
+    }
+}
+
+void
+Surface::apply_rw(GeometryComponentFilter* filter)
+{
+    filter->filter_rw(this);
+    getExteriorRing()->apply_rw(filter);
+    for (std::size_t i = 0; !filter->isDone() && i < getNumInteriorRing(); i++) {
+        getInteriorRingN(i)->apply_rw(filter);
+    }
+}
+
+void
+Surface::apply_rw(CoordinateSequenceFilter& filter)
+{
+    getExteriorRing()->apply_rw(filter);
+
+    for (std::size_t i = 0; !filter.isDone() && i < getNumInteriorRing(); i++) {
+        getInteriorRingN(i)->apply_rw(filter);
+    }
+
+    if (filter.isGeometryChanged()) {
+        geometryChanged();
+    }
+}
+
+void
+Surface::apply_ro(CoordinateSequenceFilter& filter) const
+{
+    getExteriorRing()->apply_ro(filter);
+
+    for (std::size_t i = 0; !filter.isDone() && i < getNumInteriorRing(); i++) {
+        getInteriorRingN(i)->apply_ro(filter);
+    }
+}
+
+int
+Surface::compareToSameClass(const Geometry* g) const
+{
+    const Surface* p = detail::down_cast<const Surface*>(g);
+    int shellComp = getExteriorRing()->compareTo(p->getExteriorRing());
+    if (shellComp != 0) {
+        return shellComp;
+    }
+
+    size_t nHole1 = getNumInteriorRing();
+    size_t nHole2 = p->getNumInteriorRing();
+    if (nHole1 < nHole2) {
+        return -1;
+    }
+    if (nHole1 > nHole2) {
+        return 1;
+    }
+
+    for (size_t i=0; i < nHole1; i++) {
+        const Curve* lr = p->getInteriorRingN(i);
+        const int holeComp = getInteriorRingN(i)->compareTo(lr);
+        if (holeComp != 0) {
+            return holeComp;
+        }
+    }
+
+    return 0;
+}
+
+std::unique_ptr<Geometry>
+Surface::convexHull() const
+{
+    return getExteriorRing()->convexHull();
+}
+
+
+bool
+Surface::equalsExact(const Geometry* other, double tolerance) const
+{
+    if (!isEquivalentClass(other)) {
+        return false;
+    }
+
+    const Surface* otherPolygon = detail::down_cast<const Surface*>(other);
+    if (! otherPolygon) {
+        return false;
+    }
+
+    if (!getExteriorRing()->equalsExact(otherPolygon->getExteriorRing(), tolerance)) {
+        return false;
+    }
+
+    if (getNumInteriorRing() != otherPolygon->getNumInteriorRing()) {
+        return false;
+    }
+
+    for (std::size_t i = 0; i < getNumInteriorRing(); i++) {
+        const Curve* hole = getInteriorRingN(i);
+        const Curve* otherhole = otherPolygon->getInteriorRingN(i);
+        if (!hole->equalsExact(otherhole, tolerance)) {
+            return false;
+        }
+    }
+
+    return true;
+}
+
+bool
+Surface::equalsIdentical(const Geometry* other_g) const
+{
+    if (!isEquivalentClass(other_g)) {
+        return false;
+    }
+
+    const auto& other = static_cast<const Surface&>(*other_g);
+
+    if (getNumInteriorRing() != other.getNumInteriorRing()) {
+        return false;
+    }
+
+    if (!getExteriorRing()->equalsIdentical(other.getExteriorRing())) {
+        return false;
+    }
+
+    for (std::size_t i = 0; i < getNumInteriorRing(); i++) {
+        if (!getInteriorRingN(i)->equalsIdentical(other.getInteriorRingN(i))) {
+            return false;
+        }
+    }
+
+    return true;
+}
+
+const CoordinateXY*
+Surface::getCoordinate() const
+{
+    return getExteriorRing()->getCoordinate();
+}
+
+uint8_t
+Surface::getCoordinateDimension() const
+{
+    uint8_t dimension = 2;
+
+    if (getExteriorRing() != nullptr) {
+        dimension = std::max(dimension, getExteriorRing()->getCoordinateDimension());
+    }
+
+    for (std::size_t i = 0; i < getNumInteriorRing(); i++) {
+        dimension = std::max(dimension, getInteriorRingN(i)->getCoordinateDimension());
+    }
+
+    return dimension;
+}
+
+const Envelope*
+Surface::getEnvelopeInternal() const
+{
+    return getExteriorRing()->getEnvelopeInternal();
+}
+
+size_t
+Surface::getNumPoints() const
+{
+    std::size_t numPoints = getExteriorRing()->getNumPoints();
+    for (std::size_t i = 0; i < getNumInteriorRing(); i++) {
+        numPoints += getInteriorRingN(i)->getNumPoints();
+    }
+    return numPoints;
+}
+
+bool
+Surface::hasM() const
+{
+    if (getExteriorRing()->hasM()) {
+        return true;
+    }
+    for (std::size_t i = 0 ; i < getNumInteriorRing(); i++) {
+        if (getInteriorRingN(i)->hasM()) {
+            return true;
+        }
+    }
+    return false;
+}
+
+bool
+Surface::hasZ() const
+{
+    if (getExteriorRing()->hasZ()) {
+        return true;
+    }
+    for (std::size_t i = 0 ; i < getNumInteriorRing(); i++) {
+        if (getInteriorRingN(i)->hasZ()) {
+            return true;
+        }
+    }
+    return false;
+}
+
+bool
+Surface::isEmpty() const
+{
+    return getExteriorRing()->isEmpty();
+}
+
+std::unique_ptr<Geometry>
+Surface::createEmptyRing(const GeometryFactory& factory)
+{
+    return factory.createLinearRing();
+}
+
+}
+}
diff --git a/src/io/WKTReader.cpp b/src/io/WKTReader.cpp
index 3ed640630..f72bedede 100644
--- a/src/io/WKTReader.cpp
+++ b/src/io/WKTReader.cpp
@@ -22,14 +22,20 @@
 #include <geos/io/ParseException.h>
 #include <geos/io/CLocalizer.h>
 #include <geos/geom/Coordinate.h>
+#include <geos/geom/CircularString.h>
+#include <geos/geom/CompoundCurve.h>
 #include <geos/geom/Point.h>
 #include <geos/geom/LinearRing.h>
 #include <geos/geom/LineString.h>
 #include <geos/geom/Polygon.h>
+#include <geos/geom/CurvePolygon.h>
 #include <geos/geom/MultiPoint.h>
 #include <geos/geom/MultiLineString.h>
+#include <geos/geom/MultiCurve.h>
 #include <geos/geom/MultiPolygon.h>
+#include <geos/geom/MultiSurface.h>
 #include <geos/geom/CoordinateSequence.h>
+#include <geos/geom/Surface.h>
 #include <geos/util.h>
 #include <geos/util/string.h>
 
@@ -277,18 +283,33 @@ WKTReader::readGeometryTaggedText(StringTokenizer* tokenizer, OrdinateSet& ordin
     else if(isTypeName(type, "LINEARRING")) {
         geom = readLinearRingText(tokenizer, newFlags);
     }
+    else if(isTypeName(type, "CIRCULARSTRING")) {
+        geom = readCircularStringText(tokenizer, newFlags);
+    }
+    else if(isTypeName(type, "COMPOUNDCURVE")) {
+        geom = readCompoundCurveText(tokenizer, newFlags);
+    }
     else if(isTypeName(type, "POLYGON")) {
         geom = readPolygonText(tokenizer, newFlags);
     }
+    else if(isTypeName(type, "CURVEPOLYGON")) {
+        geom = readCurvePolygonText(tokenizer, newFlags);
+    }
     else if(isTypeName(type,  "MULTIPOINT")) {
         geom = readMultiPointText(tokenizer, newFlags);
     }
     else if(isTypeName(type, "MULTILINESTRING")) {
         geom = readMultiLineStringText(tokenizer, newFlags);
     }
+    else if(isTypeName(type, "MULTICURVE")) {
+        geom = readMultiCurveText(tokenizer, newFlags);
+    }
     else if(isTypeName(type, "MULTIPOLYGON")) {
         geom = readMultiPolygonText(tokenizer, newFlags);
     }
+    else if(isTypeName(type, "MULTISURFACE")) {
+        geom = readMultiSurfaceText(tokenizer, newFlags);
+    }
     else if(isTypeName(type, "GEOMETRYCOLLECTION")) {
         geom = readGeometryCollectionText(tokenizer, newFlags);
     } else {
@@ -327,6 +348,67 @@ WKTReader::readLinearRingText(StringTokenizer* tokenizer, OrdinateSet& ordinateF
     return geometryFactory->createLinearRing(std::move(coords));
 }
 
+std::unique_ptr<CircularString>
+WKTReader::readCircularStringText(StringTokenizer* tokenizer, OrdinateSet& ordinateFlags) const
+{
+    auto&& coords = getCoordinates(tokenizer, ordinateFlags);
+    return geometryFactory->createCircularString(std::move(coords));
+}
+
+std::unique_ptr<Curve>
+WKTReader::readCurveText(StringTokenizer* tokenizer, OrdinateSet& ordinateFlags) const
+{
+    int type = tokenizer->peekNextToken();
+    if (type == '(') {
+        return readLineStringText(tokenizer, ordinateFlags);
+    }
+
+    auto component = readGeometryTaggedText(tokenizer, ordinateFlags);
+    if (dynamic_cast<Curve*>(component.get())) {
+        return std::unique_ptr<Curve>(static_cast<Curve*>(component.release()));
+    }
+
+    throw ParseException("Expected LINESTRING/CIRCULARSTRING/COMPOUNDCURVE but got " + component->getGeometryType());
+}
+
+std::unique_ptr<Geometry>
+WKTReader::readSurfaceText(StringTokenizer* tokenizer, OrdinateSet& ordinateFlags) const
+{
+    int type = tokenizer->peekNextToken();
+    if (type == '(') {
+        return readPolygonText(tokenizer, ordinateFlags);
+    }
+
+    auto component = readGeometryTaggedText(tokenizer, ordinateFlags);
+    if (dynamic_cast<CurvePolygon*>(component.get())) {
+        return component;
+    }
+
+    throw ParseException("Expected POLYGON or CURVEPOLYGON but got " + component->getGeometryType());
+}
+
+std::unique_ptr<CompoundCurve>
+WKTReader::readCompoundCurveText(StringTokenizer* tokenizer, OrdinateSet& ordinateFlags) const
+{
+    std::string nextToken = getNextEmptyOrOpener(tokenizer, ordinateFlags);
+    if (nextToken == "EMPTY") {
+        return geometryFactory->createCompoundCurve();
+    }
+
+    std::vector<std::unique_ptr<SimpleCurve>> curves;
+    do {
+        auto curve = readCurveText(tokenizer, ordinateFlags);
+        if (dynamic_cast<SimpleCurve*>(curve.get())) {
+            curves.emplace_back(static_cast<SimpleCurve*>(curve.release()));
+        } else {
+            throw ParseException("Expected LINESTRING or CIRCULARSTRING but got " + curve->getGeometryType());
+        }
+        nextToken = getNextCloserOrComma(tokenizer);
+    } while (nextToken == ",");
+
+    return geometryFactory->createCompoundCurve(std::move(curves));
+}
+
 std::unique_ptr<MultiPoint>
 WKTReader::readMultiPointText(StringTokenizer* tokenizer, OrdinateSet& ordinateFlags) const
 {
@@ -418,6 +500,27 @@ WKTReader::readPolygonText(StringTokenizer* tokenizer, OrdinateSet& ordinateFlag
     return geometryFactory->createPolygon(std::move(shell), std::move(holes));
 }
 
+std::unique_ptr<CurvePolygon>
+WKTReader::readCurvePolygonText(StringTokenizer* tokenizer, OrdinateSet& ordinateFlags) const
+{
+    std::string nextToken = getNextEmptyOrOpener(tokenizer, ordinateFlags);
+    if(nextToken == "EMPTY") {
+        auto coords = detail::make_unique<CoordinateSequence>(0u, ordinateFlags.hasZ(), ordinateFlags.hasM());
+        std::unique_ptr<Curve> ring = geometryFactory->createLinearRing(std::move(coords));
+        return geometryFactory->createCurvePolygon(std::move(ring));
+    }
+
+    std::vector<std::unique_ptr<Curve>> holes;
+    auto shell = readCurveText(tokenizer, ordinateFlags);
+    nextToken = getNextCloserOrComma(tokenizer);
+    while(nextToken == ",") {
+        holes.push_back(readCurveText(tokenizer, ordinateFlags));
+        nextToken = getNextCloserOrComma(tokenizer);
+    }
+
+    return geometryFactory->createCurvePolygon(std::move(shell), std::move(holes));
+}
+
 std::unique_ptr<MultiLineString>
 WKTReader::readMultiLineStringText(StringTokenizer* tokenizer, OrdinateSet& ordinateFlags) const
 {
@@ -435,6 +538,23 @@ WKTReader::readMultiLineStringText(StringTokenizer* tokenizer, OrdinateSet& ordi
     return geometryFactory->createMultiLineString(std::move(lineStrings));
 }
 
+std::unique_ptr<MultiCurve>
+WKTReader::readMultiCurveText(StringTokenizer* tokenizer, OrdinateSet& ordinateFlags) const
+{
+    std::string nextToken = getNextEmptyOrOpener(tokenizer, ordinateFlags);
+    if(nextToken == "EMPTY") {
+        return geometryFactory->createMultiCurve();
+    }
+
+    std::vector<std::unique_ptr<Curve>> curves;
+    do {
+        curves.push_back(readCurveText(tokenizer, ordinateFlags));
+        nextToken = getNextCloserOrComma(tokenizer);
+    } while(nextToken == ",");
+
+    return geometryFactory->createMultiCurve(std::move(curves));
+}
+
 std::unique_ptr<MultiPolygon>
 WKTReader::readMultiPolygonText(StringTokenizer* tokenizer, OrdinateSet& ordinateFlags) const
 {
@@ -452,6 +572,23 @@ WKTReader::readMultiPolygonText(StringTokenizer* tokenizer, OrdinateSet& ordinat
     return geometryFactory->createMultiPolygon(std::move(polygons));
 }
 
+std::unique_ptr<MultiSurface>
+WKTReader::readMultiSurfaceText(StringTokenizer* tokenizer, OrdinateSet& ordinateFlags) const
+{
+    std::string nextToken = getNextEmptyOrOpener(tokenizer, ordinateFlags);
+    if(nextToken == "EMPTY") {
+        return geometryFactory->createMultiSurface();
+    }
+
+    std::vector<std::unique_ptr<Geometry>> surfaces;
+    do {
+        surfaces.push_back(readSurfaceText(tokenizer, ordinateFlags));
+        nextToken = getNextCloserOrComma(tokenizer);
+    } while(nextToken == ",");
+
+    return geometryFactory->createMultiSurface(std::move(surfaces));
+}
+
 std::unique_ptr<GeometryCollection>
 WKTReader::readGeometryCollectionText(StringTokenizer* tokenizer, OrdinateSet& ordinateFlags) const
 {
diff --git a/src/io/WKTWriter.cpp b/src/io/WKTWriter.cpp
index e4f62573f..86c12cdf8 100644
--- a/src/io/WKTWriter.cpp
+++ b/src/io/WKTWriter.cpp
@@ -245,6 +245,11 @@ WKTWriter::appendGeometryTaggedText(const Geometry& geometry,
         case GEOS_MULTILINESTRING:    appendMultiLineStringTaggedText(static_cast<const MultiLineString&>(geometry), outputOrdinates, p_level, writer); break;
         case GEOS_MULTIPOLYGON:       appendMultiPolygonTaggedText(static_cast<const MultiPolygon&>(geometry), outputOrdinates, p_level, writer); break;
         case GEOS_GEOMETRYCOLLECTION: appendGeometryCollectionTaggedText(static_cast<const GeometryCollection&>(geometry), outputOrdinates, p_level, writer); break;
+        case GEOS_CIRCULARSTRING:
+        case GEOS_CURVEPOLYGON:
+        case GEOS_MULTICURVE:
+        case GEOS_MULTISURFACE:
+        case GEOS_COMPOUNDCURVE: throw std::runtime_error("Not handled yet");
     }
 }
 
diff --git a/src/operation/valid/IsValidOp.cpp b/src/operation/valid/IsValidOp.cpp
index 39571cc58..813ab8454 100644
--- a/src/operation/valid/IsValidOp.cpp
+++ b/src/operation/valid/IsValidOp.cpp
@@ -102,6 +102,12 @@ IsValidOp::isValidGeometry(const Geometry* g)
             return isValid(static_cast<const GeometryCollection*>(g));
         case GEOS_GEOMETRYCOLLECTION:
             return isValid(static_cast<const GeometryCollection*>(g));
+        case GEOS_CIRCULARSTRING:
+        case GEOS_COMPOUNDCURVE:
+        case GEOS_CURVEPOLYGON:
+        case GEOS_MULTICURVE:
+        case GEOS_MULTISURFACE:
+            throw util::IllegalArgumentException("Curved types not supported in IsValidOp.");
     }
 
     // geometry type not known
diff --git a/tests/unit/io/WKTReaderTest.cpp b/tests/unit/io/WKTReaderTest.cpp
index 59e5f1b25..55a247d52 100644
--- a/tests/unit/io/WKTReaderTest.cpp
+++ b/tests/unit/io/WKTReaderTest.cpp
@@ -473,4 +473,86 @@ void object::test<24>
     ensure_equals(geom->getNumGeometries(), 3u);
 }
 
+// Read a CircularString
+template<>
+template<>
+void object::test<25>
+()
+{
+    auto geom = wktreader.read("CIRCULARSTRING (0 0, 1 1, 2 0)");
+
+    ensure_equals(geom->getGeometryTypeId(), geos::geom::GEOS_CIRCULARSTRING);
+    ensure_equals(geom->getNumPoints(), 3u);
+}
+
+// Read a CompoundCurve
+template<>
+template<>
+void object::test<26>
+()
+{
+    auto geom = wktreader.read("COMPOUNDCURVE (CIRCULARSTRING (0 0, 1 1, 1 0), (1 0, 0 1))");
+
+    ensure_equals(geom->getGeometryTypeId(), geos::geom::GEOS_COMPOUNDCURVE);
+    ensure_equals(geom->getNumPoints(), 5u);
+
+    ensure_equals(geom->getNumGeometries(), 2u);
+}
+
+// Read a CurvePolygon whose components are simple curves
+template<>
+template<>
+void object::test<27>
+()
+{
+    auto geom = wktreader.read("CURVEPOLYGON( CIRCULARSTRING(0 0, 4 0, 4 4, 0 4, 0 0), (1 1, 3 3, 3 1, 1 1) )");
+
+    ensure_equals(geom->getGeometryTypeId(), geos::geom::GEOS_CURVEPOLYGON);
+}
+
+// Read a CurvePolygon whose components include compound curves
+template<>
+template<>
+void object::test<28>
+()
+{
+    auto geom = wktreader.read("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) )");
+
+    ensure_equals(geom->getGeometryTypeId(), geos::geom::GEOS_CURVEPOLYGON);
+}
+
+// Read a MultiCurve
+template<>
+template<>
+void object::test<29>
+()
+{
+    auto geom = wktreader.read("MULTICURVE( (0 0, 5 5), CIRCULARSTRING(4 0, 4 4, 8 4))");
+
+    ensure_equals(geom->getGeometryTypeId(), geos::geom::GEOS_MULTICURVE);
+}
+
+// Read a MultiCurve whose elements contain CompoundCurves
+template<>
+template<>
+void object::test<30>
+()
+{
+    auto geom = wktreader.read("MULTICURVE( (0 0, 5 5), COMPOUNDCURVE( (0 0, 1 1), CIRCULARSTRING (0 0, 1 1, 2 0)), CIRCULARSTRING(4 0, 4 4, 8 4))");
+
+    ensure_equals(geom->getGeometryTypeId(), geos::geom::GEOS_MULTICURVE);
+}
+
+// Read a MultiSurface
+template<>
+template<>
+void object::test<31>
+()
+{
+    auto geom = wktreader.read("MULTISURFACE( CURVEPOLYGON( CIRCULARSTRING( 0 0, 4 0, 4 4, 0 4, 0 0), (1 1, 3 3, 3 1, 1 1)), ((10 10, 14 12, 11 10, 10 10), (11 11, 11.5 11, 11 11.5, 11 11)))");
+
+    ensure_equals(geom->getGeometryTypeId(), geos::geom::GEOS_MULTISURFACE);
+}
+
+
 } // namespace tut

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

Summary of changes:
 CMakeLists.txt                                     |  32 ++
 NEWS.md                                            |   9 +-
 Version.txt                                        |   4 +-
 benchmarks/operation/CMakeLists.txt                |  10 +
 benchmarks/operation/DistancePerfTest.cpp          |  80 +++++
 capi/geos_c.h.in                                   |   7 +-
 capi/geos_ts_c.cpp                                 |  39 +-
 include/geos/algorithm/CircularArcs.h              |  37 ++
 include/geos/algorithm/ConvexHull.h                |   6 +-
 include/geos/algorithm/LineIntersector.h           |  11 -
 include/geos/algorithm/PointLocation.h             |  10 +
 include/geos/algorithm/hull/ConcaveHull.h          |  10 +-
 include/geos/geom/CircularString.h                 |  85 +++++
 include/geos/geom/CompoundCurve.h                  | 121 +++++++
 include/geos/geom/Curve.h                          |  65 ++++
 include/geos/geom/CurvePolygon.h                   |  58 +++
 include/geos/geom/Geometry.h                       |  46 ++-
 include/geos/geom/GeometryCollection.h             |   2 +
 include/geos/geom/GeometryFactory.h                |  63 +++-
 include/geos/geom/LineString.h                     | 120 +------
 include/geos/geom/MultiCurve.h                     | 126 +++++++
 include/geos/geom/MultiLineString.h                |   5 +
 include/geos/geom/MultiPoint.h                     |   5 +
 include/geos/geom/MultiPolygon.h                   |   7 +-
 include/geos/geom/MultiSurface.h                   |  94 +++++
 include/geos/geom/Polygon.h                        | 129 +------
 include/geos/geom/SimpleCurve.h                    | 136 +++++++
 include/geos/geom/Surface.h                        | 115 ++++++
 include/geos/geom/SurfaceImpl.h                    | 159 +++++++++
 include/geos/geomgraph/DirectedEdgeStar.h          |   3 +-
 include/geos/geomgraph/EdgeEndStar.h               |   7 +-
 include/geos/io/WKTReader.h                        |  19 +-
 include/geos/io/WKTWriter.h                        |  54 +--
 include/geos/operation/GeometryGraphOperation.h    |   6 +-
 .../distance/ConnectedElementLocationFilter.h      |   4 +-
 include/geos/operation/distance/DistanceOp.h       |  18 +-
 include/geos/operation/distance/GeometryLocation.h |   7 +
 include/geos/operation/polygonize/Polygonizer.h    |  10 -
 include/geos/operation/relate/RelateComputer.h     |   4 +-
 include/geos/operation/union/UnaryUnionOp.h        |   4 +
 .../polygon/ConstrainedDelaunayTriangulator.h      |   2 +-
 include/geos/util.h                                |  22 ++
 include/geos/util/string.h                         |   4 +-
 src/algorithm/Centroid.cpp                         |   8 +
 src/algorithm/CircularArcs.cpp                     | 124 +++++++
 src/algorithm/LineIntersector.cpp                  |  34 --
 src/algorithm/PointLocation.cpp                    |  23 +-
 src/algorithm/distance/DiscreteFrechetDistance.cpp |   5 +
 .../distance/DiscreteHausdorffDistance.cpp         |   5 +
 src/algorithm/hull/ConcaveHull.cpp                 |  12 +
 src/algorithm/hull/ConcaveHullOfPolygons.cpp       |   3 +
 src/coverage/CoverageRingEdges.cpp                 |   2 +
 src/coverage/CoverageValidator.cpp                 |   2 +
 src/geom/CircularString.cpp                        |  80 +++++
 src/geom/CompoundCurve.cpp                         | 311 ++++++++++++++++
 src/geom/Curve.cpp                                 |  59 +++
 src/geom/CurvePolygon.cpp                          |  87 +++++
 src/geom/Geometry.cpp                              |  79 +---
 src/geom/GeometryCollection.cpp                    |   9 +
 src/geom/GeometryFactory.cpp                       | 178 ++++++++-
 src/geom/LineString.cpp                            | 354 +-----------------
 src/geom/MultiCurve.cpp                            | 116 ++++++
 src/geom/MultiSurface.cpp                          | 109 ++++++
 src/geom/Polygon.cpp                               | 396 ++-------------------
 src/geom/SimpleCurve.cpp                           | 359 +++++++++++++++++++
 src/geom/Surface.cpp                               | 286 +++++++++++++++
 src/geom/prep/BasicPreparedGeometry.cpp            |   2 +
 src/geom/prep/PreparedGeometryFactory.cpp          |   2 +
 src/geom/prep/PreparedLineString.cpp               |   2 +
 src/geom/prep/PreparedPoint.cpp                    |   5 +
 src/geom/prep/PreparedPolygon.cpp                  |   2 +
 src/geom/util/GeometryEditor.cpp                   |   2 +
 src/geom/util/LinearComponentExtracter.cpp         |   4 +
 src/geom/util/PointExtracter.cpp                   |  12 +-
 src/geom/util/PolygonExtracter.cpp                 |   4 +
 src/geomgraph/DirectedEdgeStar.cpp                 |   2 +-
 src/geomgraph/EdgeEndStar.cpp                      |   8 +-
 src/geomgraph/GeometryGraph.cpp                    |   2 +
 src/io/GeoJSONWriter.cpp                           |   4 +
 src/io/WKBWriter.cpp                               |   4 +
 src/io/WKTReader.cpp                               | 152 +++++++-
 src/io/WKTWriter.cpp                               | 205 +++++++----
 src/noding/GeometryNoder.cpp                       |   1 +
 src/operation/BoundaryOp.cpp                       |   2 +
 src/operation/GeometryGraphOperation.cpp           |  13 +-
 .../distance/ConnectedElementLocationFilter.cpp    |   6 +-
 src/operation/distance/DistanceOp.cpp              |  75 ++--
 src/operation/linemerge/LineMerger.cpp             |   2 +
 src/operation/overlayng/OverlayNGRobust.cpp        |   7 +-
 src/operation/polygonize/Polygonizer.cpp           |  15 +-
 src/operation/relate/RelateComputer.cpp            |  38 +-
 src/operation/relate/RelateOp.cpp                  |   4 +-
 src/operation/union/UnaryUnionOp.cpp               |   2 +
 src/operation/valid/IsSimpleOp.cpp                 |  15 +-
 src/operation/valid/IsValidOp.cpp                  |   6 +
 src/operation/valid/PolygonTopologyAnalyzer.cpp    |   3 +-
 src/precision/MinimumClearance.cpp                 |   4 +
 src/triangulate/DelaunayTriangulationBuilder.cpp   |   2 +
 src/triangulate/VoronoiDiagramBuilder.cpp          |   1 +
 .../polygon/ConstrainedDelaunayTriangulator.cpp    |   4 +-
 src/util/string.cpp                                |   8 +-
 tests/unit/algorithm/CentroidTest.cpp              |   6 +
 tests/unit/algorithm/CircularArcsTest.cpp          | 219 ++++++++++++
 tests/unit/algorithm/PointLocationTest.cpp         | 129 +++++++
 tests/unit/capi/GEOSBoundaryTest.cpp               |  10 +
 tests/unit/capi/GEOSBufferTest.cpp                 |  11 +
 tests/unit/capi/GEOSBuildAreaTest.cpp              |  11 +
 tests/unit/capi/GEOSClipByRectTest.cpp             |  11 +
 tests/unit/capi/GEOSConcaveHullOfPolygonsTest.cpp  |  11 +
 tests/unit/capi/GEOSConcaveHullTest.cpp            |  11 +
 .../GEOSConstrainedDelaunayTriangulationTest.cpp   |  11 +
 tests/unit/capi/GEOSContainsTest.cpp               |  14 +
 tests/unit/capi/GEOSConvexHullTest.cpp             |  11 +
 tests/unit/capi/GEOSCoverageIsValidTest.cpp        |  10 +
 tests/unit/capi/GEOSCoverageSimplifyTest.cpp       |  12 +
 tests/unit/capi/GEOSCoverageUnionTest.cpp          |  16 +-
 tests/unit/capi/GEOSCoveredByTest.cpp              |  13 +
 tests/unit/capi/GEOSCoversTest.cpp                 |  13 +
 tests/unit/capi/GEOSCrossesTest.cpp                |  14 +
 tests/unit/capi/GEOSDelaunayTriangulationTest.cpp  |  11 +
 tests/unit/capi/GEOSDensifyTest.cpp                |  10 +
 tests/unit/capi/GEOSDifferenceTest.cpp             |  17 +-
 tests/unit/capi/GEOSDisjointSubsetUnionTest.cpp    |   9 +
 tests/unit/capi/GEOSDisjointTest.cpp               |  14 +
 tests/unit/capi/GEOSDistanceTest.cpp               |  15 +
 tests/unit/capi/GEOSDistanceWithinTest.cpp         |  13 +
 tests/unit/capi/GEOSEqualsIdenticalTest.cpp        |  11 +
 tests/unit/capi/GEOSEqualsTest.cpp                 |  14 +
 tests/unit/capi/GEOSFrechetDistanceTest.cpp        |  15 +
 tests/unit/capi/GEOSGeomGeoJSONWriteTest.cpp       |  13 +
 tests/unit/capi/GEOSGeomGetNumPointsTest.cpp       |   8 +
 tests/unit/capi/GEOSGeomToHEX_bufTest.cpp          |   2 +
 tests/unit/capi/GEOSGeomTypeIdTest.cpp             |   7 +-
 tests/unit/capi/GEOSGeomTypeTest.cpp               |   5 +
 tests/unit/capi/GEOSGeom_cloneTest.cpp             |  10 +
 .../unit/capi/GEOSGeom_extractUniquePointsTest.cpp |  11 +
 tests/unit/capi/GEOSGeom_getCoordSeqTest.cpp       |  14 +
 .../capi/GEOSGeom_getCoordinateDimensionTest.cpp   |  33 +-
 tests/unit/capi/GEOSGeom_getDimensionsTest.cpp     |   8 +
 tests/unit/capi/GEOSGeom_transformXYTest.cpp       |  11 +
 tests/unit/capi/GEOSGetCentroidTest.cpp            |  11 +
 tests/unit/capi/GEOSGetExteriorRingTest.cpp        |  13 +
 tests/unit/capi/GEOSGetGeometryNTest.cpp           |  15 +-
 tests/unit/capi/GEOSGetInteriorRingNTest.cpp       |  12 +
 tests/unit/capi/GEOSGetNumInteriorRingsTest.cpp    |  10 +
 tests/unit/capi/GEOSHasZMTest.cpp                  |   9 +
 tests/unit/capi/GEOSHausdorffDistanceTest.cpp      |  15 +
 tests/unit/capi/GEOSIntersectionTest.cpp           |  13 +
 tests/unit/capi/GEOSIntersectsTest.cpp             |  14 +
 tests/unit/capi/GEOSLargestEmptyCircleTest.cpp     |  27 ++
 tests/unit/capi/GEOSLengthTest.cpp                 |  12 +
 tests/unit/capi/GEOSLineMergeDirectedTest.cpp      |  13 +
 tests/unit/capi/GEOSLineMergeTest.cpp              |  12 +
 tests/unit/capi/GEOSLineSubstringTest.cpp          |  11 +
 tests/unit/capi/GEOSMakeValidTest.cpp              |  11 +
 tests/unit/capi/GEOSMaximumInscribedCircleTest.cpp |   9 +
 tests/unit/capi/GEOSMinimumBoundingCircleTest.cpp  |  14 +
 tests/unit/capi/GEOSMinimumClearanceTest.cpp       |  10 +
 .../unit/capi/GEOSMinimumRotatedRectangleTest.cpp  |  11 +
 tests/unit/capi/GEOSMinimumWidthTest.cpp           |  12 +
 tests/unit/capi/GEOSNodeTest.cpp                   |  12 +
 tests/unit/capi/GEOSOffsetCurveTest.cpp            |  12 +
 tests/unit/capi/GEOSOrientPolygonsTest.cpp         |  11 +
 tests/unit/capi/GEOSOverlapsTest.cpp               |  14 +
 tests/unit/capi/GEOSPointOnSurfaceTest.cpp         |  12 +
 tests/unit/capi/GEOSPolygonHullSimplifyTest.cpp    |  10 +
 tests/unit/capi/GEOSPolygonizeTest.cpp             |  23 ++
 tests/unit/capi/GEOSPreparedGeometryTest.cpp       |  12 +
 tests/unit/capi/GEOSRelatePatternTest.cpp          |  14 +
 tests/unit/capi/GEOSRelateTest.cpp                 |  14 +
 tests/unit/capi/GEOSRemoveRepeatedPointsTest.cpp   |  11 +
 tests/unit/capi/GEOSReverseTest.cpp                |   8 +
 tests/unit/capi/GEOSSharedPathsTest.cpp            |  14 +
 tests/unit/capi/GEOSSimplifyTest.cpp               |  11 +
 tests/unit/capi/GEOSSnapTest.cpp                   |  14 +
 tests/unit/capi/GEOSSymDifferenceTest.cpp          |  14 +
 .../unit/capi/GEOSTopologyPreserveSimplifyTest.cpp |  11 +
 tests/unit/capi/GEOSTouchesTest.cpp                |  14 +
 tests/unit/capi/GEOSUnaryUnionTest.cpp             |  12 +
 tests/unit/capi/GEOSUnionTest.cpp                  |  14 +
 tests/unit/capi/GEOSVoronoiDiagramTest.cpp         |  12 +
 tests/unit/capi/GEOSWKBWriterTest.cpp              |  13 +
 tests/unit/capi/GEOSWithinTest.cpp                 |  13 +
 tests/unit/capi/GEOSisClosedTest.cpp               |  22 +-
 tests/unit/capi/GEOSisRingTest.cpp                 |  22 +-
 tests/unit/capi/GEOSisSimpleTest.cpp               |  11 +
 tests/unit/capi/GEOSisValidDetailTest.cpp          |  11 +
 tests/unit/capi/GEOSisValidReasonTest.cpp          |  11 +
 tests/unit/capi/GEOSisValidTest.cpp                |  11 +
 tests/unit/capi/capi_test_utils.h                  |   2 +-
 tests/unit/geom/CircularStringTest.cpp             | 178 +++++++++
 tests/unit/geom/CompoundCurveTest.cpp              | 319 +++++++++++++++++
 tests/unit/geom/CurvePolygonTest.cpp               | 186 ++++++++++
 tests/unit/geom/MultiCurveTest.cpp                 | 199 +++++++++++
 tests/unit/geom/MultiSurfaceTest.cpp               | 170 +++++++++
 tests/unit/io/WKTReaderTest.cpp                    |  92 +++++
 tests/unit/io/WKTWriterTest.cpp                    | 247 ++++++++++++-
 web/content/specifications/wkb.md                  |   7 +
 198 files changed, 6466 insertions(+), 1371 deletions(-)
 create mode 100644 benchmarks/operation/DistancePerfTest.cpp
 create mode 100644 include/geos/algorithm/CircularArcs.h
 create mode 100644 include/geos/geom/CircularString.h
 create mode 100644 include/geos/geom/CompoundCurve.h
 create mode 100644 include/geos/geom/Curve.h
 create mode 100644 include/geos/geom/CurvePolygon.h
 create mode 100644 include/geos/geom/MultiCurve.h
 create mode 100644 include/geos/geom/MultiSurface.h
 create mode 100644 include/geos/geom/SimpleCurve.h
 create mode 100644 include/geos/geom/Surface.h
 create mode 100644 include/geos/geom/SurfaceImpl.h
 create mode 100644 src/algorithm/CircularArcs.cpp
 create mode 100644 src/geom/CircularString.cpp
 create mode 100644 src/geom/CompoundCurve.cpp
 create mode 100644 src/geom/Curve.cpp
 create mode 100644 src/geom/CurvePolygon.cpp
 create mode 100644 src/geom/MultiCurve.cpp
 create mode 100644 src/geom/MultiSurface.cpp
 create mode 100644 src/geom/SimpleCurve.cpp
 create mode 100644 src/geom/Surface.cpp
 create mode 100644 tests/unit/algorithm/CircularArcsTest.cpp
 create mode 100644 tests/unit/algorithm/PointLocationTest.cpp
 create mode 100644 tests/unit/geom/CircularStringTest.cpp
 create mode 100644 tests/unit/geom/CompoundCurveTest.cpp
 create mode 100644 tests/unit/geom/CurvePolygonTest.cpp
 create mode 100644 tests/unit/geom/MultiCurveTest.cpp
 create mode 100644 tests/unit/geom/MultiSurfaceTest.cpp


hooks/post-receive
-- 
GEOS


More information about the geos-commits mailing list