[geos-commits] [SCM] GEOS branch main updated. 0154023f7e0d15a27d2bf38fe6b78408be560bc8

git at osgeo.org git at osgeo.org
Thu Jan 29 07:11:41 PST 2026


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

The branch, main has been updated
       via  0154023f7e0d15a27d2bf38fe6b78408be560bc8 (commit)
       via  79ed12458622b265a89efaa10e680c74442dc5c3 (commit)
       via  35bd4a66da3dffc85ab4579e7671e3eec8fb6776 (commit)
       via  5e1bd575034913eb1941b9fcb303cb2be2630bb3 (commit)
       via  15ce6735d6ba3014fc12fcf86df9eecf92db9739 (commit)
       via  f54ead7568472702e67563ee1d559b5d7c6d6b66 (commit)
       via  bd03a4b050c553f9c6b84ed1554abe5464cd3e20 (commit)
       via  78881d3917a5b22e916384c496ed15c931acc0f8 (commit)
       via  8447ff2a32dd20703e0841736a8a3a6bd6be88f5 (commit)
       via  3efe43f99214c8e3fa3c0e76f3c991331f897c41 (commit)
       via  114887e4058d97ead9ccc22269300ad51aa51e56 (commit)
       via  7d701f5e5077fe860298bd29802922a206b6d70e (commit)
       via  0de2f8fb56f74890702f7abd7532833b4e48f606 (commit)
       via  c181764a77beae6e05529eff91811c6a7c53f4ce (commit)
       via  4e0f6f89ba90a1940166537cc9449a4bd34e9622 (commit)
       via  bac74b25b9508a54a14fceb6bb9ed172ad10e3b4 (commit)
       via  db93f778ec51fdc1a71886f2b319ec6da35a302d (commit)
      from  8f2364fc2c3eecc915e224f5d0e8399bb96d4e4c (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 0154023f7e0d15a27d2bf38fe6b78408be560bc8
Author: Daniel Baston <dbaston at gmail.com>
Date:   Thu Jan 29 09:35:19 2026 -0500

    ArcNoder: Match semantics of Noder, do not take ownership of ArcIntersector

diff --git a/include/geos/noding/ArcNoder.h b/include/geos/noding/ArcNoder.h
index 2fd418cfa..e5f46f7fe 100644
--- a/include/geos/noding/ArcNoder.h
+++ b/include/geos/noding/ArcNoder.h
@@ -29,13 +29,13 @@ class GEOS_DLL ArcNoder : public Noder {
 public:
     ArcNoder() = default;
 
-    explicit ArcNoder(std::unique_ptr<ArcIntersector> intersector) :
-        m_intersector(std::move(intersector)) {}
+    explicit ArcNoder(ArcIntersector& intersector) :
+        m_intersector(&intersector) {}
 
     ~ArcNoder() override;
 
-    void setArcIntersector(std::unique_ptr<ArcIntersector> arcIntersector) {
-        m_intersector = std::move(arcIntersector);
+    void setArcIntersector(ArcIntersector& arcIntersector) {
+        m_intersector = &arcIntersector;
     }
 
     void computeNodes(const std::vector<SegmentString*>& segStrings) override;
@@ -47,7 +47,7 @@ public:
     virtual std::vector<std::unique_ptr<PathString>> getNodedPaths() = 0;
 
 protected:
-    std::unique_ptr<ArcIntersector> m_intersector;
+    ArcIntersector* m_intersector;
 };
 
 }
\ No newline at end of file
diff --git a/src/noding/GeometryNoder.cpp b/src/noding/GeometryNoder.cpp
index dbc02bf25..cab42cb03 100644
--- a/src/noding/GeometryNoder.cpp
+++ b/src/noding/GeometryNoder.cpp
@@ -157,11 +157,9 @@ GeometryNoder::getNoded()
     if (argGeomHasCurves) {
         ArcNoder& p_noder = static_cast<ArcNoder&>(getNoder());
 
-        // TODO: Improve lifecycle here. We have a heap-allocated ArcIntersectionAdder referencing
-        // a stack-allocated CircularArcIntersector.
         algorithm::CircularArcIntersector cai(argGeom.getPrecisionModel());
-        auto arcIntersector = std::make_unique<ArcIntersectionAdder>(cai);
-        p_noder.setArcIntersector(std::move(arcIntersector));
+        ArcIntersectionAdder aia(cai);
+        p_noder.setArcIntersector(aia);
 
         p_noder.computePathNodes(PathString::toRawPointerVector(lineList));
         nodedEdges = p_noder.getNodedPaths();
diff --git a/tests/unit/noding/SimpleNoderTest.cpp b/tests/unit/noding/SimpleNoderTest.cpp
index 934247541..0625f150d 100644
--- a/tests/unit/noding/SimpleNoderTest.cpp
+++ b/tests/unit/noding/SimpleNoderTest.cpp
@@ -123,7 +123,8 @@ void object::test<1>()
 
     std::vector<PathString*> ss{&ss1, &ss2};
 
-    SimpleNoder noder(std::make_unique<geos::noding::ArcIntersectionAdder>(cai));
+    geos::noding::ArcIntersectionAdder aia(cai);
+    SimpleNoder noder(aia);
     noder.computePathNodes(ss);
 
     auto paths = noder.getNodedPaths();
@@ -149,7 +150,8 @@ void object::test<2>()
 
     std::vector<PathString*> ss{&as0, &as1};
 
-    SimpleNoder noder(std::make_unique<geos::noding::ArcIntersectionAdder>(cai));
+    geos::noding::ArcIntersectionAdder aia(cai);
+    SimpleNoder noder(aia);
     noder.computePathNodes(ss);
 
     auto paths = noder.getNodedPaths();
@@ -175,7 +177,8 @@ void object::test<3>()
 
     std::vector<PathString*> ss{&as0, &ss1};
 
-    SimpleNoder noder(std::make_unique<geos::noding::ArcIntersectionAdder>(cai));
+    geos::noding::ArcIntersectionAdder aia(cai);
+    SimpleNoder noder(aia);
     noder.computePathNodes(ss);
 
     auto paths = noder.getNodedPaths();

commit 79ed12458622b265a89efaa10e680c74442dc5c3
Author: Daniel Baston <dbaston at gmail.com>
Date:   Thu Jan 29 09:21:21 2026 -0500

    GeometryNoder: Use SimpleNoder directly for arcs

diff --git a/include/geos/noding/GeometryNoder.h b/include/geos/noding/GeometryNoder.h
index e31fd08cf..0938302a9 100644
--- a/include/geos/noding/GeometryNoder.h
+++ b/include/geos/noding/GeometryNoder.h
@@ -29,7 +29,7 @@ namespace geom {
 class Geometry;
 }
 namespace noding {
-class ArcNoder;
+class Noder;
 }
 }
 
@@ -56,14 +56,12 @@ private:
     const geom::Geometry& argGeom;
     const bool argGeomHasCurves;
 
-    SegmentString::NonConstVect lineList;
-
     static void extractPathStrings(const geom::Geometry& g,
                                    std::vector<std::unique_ptr<PathString>>& to);
 
-    ArcNoder& getNoder();
+    Noder& getNoder();
 
-    std::unique_ptr<ArcNoder> noder;
+    std::unique_ptr<Noder> noder;
 
     std::unique_ptr<geom::Geometry> toGeometry(std::vector<std::unique_ptr<PathString>>& noded) const;
 
diff --git a/include/geos/noding/IteratedNoder.h b/include/geos/noding/IteratedNoder.h
index 11a0ab48f..1d2bc296a 100644
--- a/include/geos/noding/IteratedNoder.h
+++ b/include/geos/noding/IteratedNoder.h
@@ -20,14 +20,12 @@
 
 #include <geos/export.h>
 
-#include <functional>
-#include <memory>
 #include <vector>
+#include <iostream>
 
-#include <geos/algorithm/CircularArcIntersector.h>
 #include <geos/algorithm/LineIntersector.h>
 #include <geos/noding/SegmentString.h> // due to inlines
-#include <geos/noding/ArcNoder.h> // for inheritance
+#include <geos/noding/Noder.h> // for inheritance
 
 // Forward declarations
 namespace geos {
@@ -51,37 +49,36 @@ namespace noding { // geos::noding
  * Clients can choose to rerun the noding using a lower precision model.
  *
  */
-class GEOS_DLL IteratedNoder : public ArcNoder { // implements Noder
+class GEOS_DLL IteratedNoder : public Noder { // implements Noder
 
 private:
-    static constexpr int MAX_ITER = 5;
+    static const int MAX_ITER = 5;
 
 
     const geom::PrecisionModel* pm;
-    algorithm::CircularArcIntersector cai;
     algorithm::LineIntersector li;
-    std::vector<std::unique_ptr<PathString>> nodedPaths;
+    std::vector<std::unique_ptr<SegmentString>> nodedSegStrings;
     int maxIter;
-    std::function<std::unique_ptr<Noder>()> m_noderFunction;
 
     /**
      * Node the input segment strings once
      * and create the split edges between the nodes
      */
-    void node(const std::vector<PathString*>& segStrings,
+    void node(const std::vector<SegmentString*>& segStrings,
               int& numInteriorIntersections,
               geom::CoordinateXY& intersectionPoint);
 
-    static std::unique_ptr<Noder> createDefaultNoder();
-
 public:
 
-    /** \brief
-     * Construct an IteratedNoder using a specific precisionModel and underlying Noder.
-     */
-    IteratedNoder(const geom::PrecisionModel* newPm, std::function<std::unique_ptr<Noder>()> noderFunction = createDefaultNoder);
+    IteratedNoder(const geom::PrecisionModel* newPm)
+        :
+        pm(newPm),
+        li(pm),
+        maxIter(MAX_ITER)
+    {
+    }
 
-    ~IteratedNoder() override;
+    ~IteratedNoder() override {}
 
     /** \brief
      * Sets the maximum number of noding iterations performed before
@@ -99,21 +96,23 @@ public:
         maxIter = n;
     }
 
-    std::vector<std::unique_ptr<PathString>> getNodedPaths() override
+    std::vector<std::unique_ptr<SegmentString>>
+    getNodedSubstrings() override
     {
-        return std::move(nodedPaths);
+        return std::move(nodedSegStrings);
     }
 
+
     /** \brief
-     * Fully nodes a list of {@link PathString}s, i.e. performs noding iteratively
+     * Fully nodes a list of {@link SegmentString}s, i.e. performs noding iteratively
      * until no intersections are found between segments.
      *
      * Maintains labelling of edges correctly through the noding.
      *
-     * @param inputPathStrings a collection of SegmentStrings to be noded
+     * @param inputSegmentStrings a collection of SegmentStrings to be noded
      * @throws TopologyException if the iterated noding fails to converge.
      */
-    void computePathNodes(const std::vector<PathString*>& inputPathStrings) override;
+    void computeNodes(const std::vector<SegmentString*>& inputSegmentStrings) override; // throw(GEOSException);
 
     // Declare type as noncopyable
     IteratedNoder(IteratedNoder const&) = delete;
diff --git a/src/noding/GeometryNoder.cpp b/src/noding/GeometryNoder.cpp
index 6103364ea..dbc02bf25 100644
--- a/src/noding/GeometryNoder.cpp
+++ b/src/noding/GeometryNoder.cpp
@@ -16,6 +16,7 @@
  *
  **********************************************************************/
 
+#include <geos/algorithm/CircularArcIntersector.h>
 #include <geos/noding/GeometryNoder.h>
 #include <geos/noding/SegmentString.h>
 #include <geos/noding/NodedSegmentString.h>
@@ -42,6 +43,8 @@
 #include <memory> // for unique_ptr
 #include <iostream>
 
+#include "geos/noding/ArcIntersectionAdder.h"
+
 namespace geos {
 namespace noding { // geos.noding
 
@@ -146,18 +149,30 @@ GeometryNoder::getNoded()
     if (argGeom.isEmpty())
         return argGeom.clone();
 
-    std::vector<std::unique_ptr<PathString>> p_lineList;
-    extractPathStrings(argGeom, p_lineList);
-
-    ArcNoder& p_noder = getNoder();
+    std::vector<std::unique_ptr<PathString>> lineList;
     std::vector<std::unique_ptr<PathString>> nodedEdges;
 
-    try {
-        p_noder.computePathNodes(PathString::toRawPointerVector(p_lineList));
+    extractPathStrings(argGeom, lineList);
+
+    if (argGeomHasCurves) {
+        ArcNoder& p_noder = static_cast<ArcNoder&>(getNoder());
+
+        // TODO: Improve lifecycle here. We have a heap-allocated ArcIntersectionAdder referencing
+        // a stack-allocated CircularArcIntersector.
+        algorithm::CircularArcIntersector cai(argGeom.getPrecisionModel());
+        auto arcIntersector = std::make_unique<ArcIntersectionAdder>(cai);
+        p_noder.setArcIntersector(std::move(arcIntersector));
+
+        p_noder.computePathNodes(PathString::toRawPointerVector(lineList));
         nodedEdges = p_noder.getNodedPaths();
-    }
-    catch(const std::exception&) {
-        throw;
+    } else {
+        Noder& p_noder = getNoder();
+        p_noder.computeNodes(SegmentString::toRawPointerVector(lineList));
+        auto nodedSegStrings = p_noder.getNodedSubstrings();
+        nodedEdges.resize(nodedSegStrings.size());
+        for (size_t i = 0; i < nodedSegStrings.size(); i++) {
+            nodedEdges[i] = std::move(nodedSegStrings[i]);
+        }
     }
 
     std::unique_ptr<geom::Geometry> noded = toGeometry(nodedEdges);
@@ -175,13 +190,13 @@ GeometryNoder::extractPathStrings(const geom::Geometry& g,
 }
 
 /* private */
-ArcNoder&
+Noder&
 GeometryNoder::getNoder()
 {
-    if(! noder.get()) {
+    if(!noder) {
         const geom::PrecisionModel* pm = argGeom.getFactory()->getPrecisionModel();
         if (argGeomHasCurves) {
-            noder = std::make_unique<IteratedNoder>(pm, []() { return std::make_unique<SimpleNoder>(); });
+            noder = std::make_unique<SimpleNoder>();
         } else {
             noder = std::make_unique<IteratedNoder>(pm);
         }
diff --git a/src/noding/IteratedNoder.cpp b/src/noding/IteratedNoder.cpp
index df1e9beaf..c88b76387 100644
--- a/src/noding/IteratedNoder.cpp
+++ b/src/noding/IteratedNoder.cpp
@@ -17,14 +17,11 @@
  *
  **********************************************************************/
 
-#include <functional>
 #include <sstream>
 #include <vector>
 
 #include <geos/profiler.h>
 #include <geos/util/TopologyException.h>
-#include <geos/noding/ArcIntersectionAdder.h>
-#include <geos/noding/ArcNoder.h>
 #include <geos/noding/IteratedNoder.h>
 #include <geos/noding/SegmentString.h>
 #include <geos/noding/MCIndexNoder.h>
@@ -34,92 +31,47 @@
 #define GEOS_DEBUG 0
 #endif
 
+using namespace geos::geom;
+
 namespace geos {
 namespace noding { // geos.noding
 
-IteratedNoder::IteratedNoder(const geom::PrecisionModel* newPm,
-                             std::function<std::unique_ptr<Noder>()> noderFunction)
-    :
-    pm(newPm),
-    cai(pm),
-    li(pm),
-    maxIter(MAX_ITER),
-    m_noderFunction(noderFunction)
-{
-}
-
-std::unique_ptr<Noder>
-IteratedNoder::createDefaultNoder()
-{
-    return std::make_unique<MCIndexNoder>();
-}
-
-IteratedNoder::~IteratedNoder() = default;
-
 /* private */
 void
-IteratedNoder::node(const std::vector<PathString*>& pathStrings,
+IteratedNoder::node(const std::vector<SegmentString*>& segStrings,
                     int& numInteriorIntersections,
-                    geom::CoordinateXY& intersectionPoint)
+                    CoordinateXY& intersectionPoint)
 {
+    IntersectionAdder si(li);
+    MCIndexNoder noder;
+    noder.setSegmentIntersector(&si);
+    noder.computeNodes(segStrings);
+    auto updatedSegStrings = noder.getNodedSubstrings();
+    nodedSegStrings = std::move(updatedSegStrings);
+    numInteriorIntersections = si.numInteriorIntersections;
 
-    auto noder = m_noderFunction();
-    if (auto* spn = dynamic_cast<SinglePassNoder*>(noder.get())) {
-        IntersectionAdder si(li);
-        spn->setSegmentIntersector(&si);
-        // TODO need to have previously checked that all inputs are SegmentStrings
-
-        std::vector<SegmentString*> segStrings(pathStrings.size());
-        for (size_t i = 0; i < pathStrings.size(); i++) {
-            segStrings[i] = detail::down_cast<SegmentString*>(pathStrings[i]);
-        }
-
-        noder->computeNodes(segStrings);
-
-        auto nodedSegStrings = noder->getNodedSubstrings();
-        nodedPaths.resize(nodedSegStrings.size());
-        for (size_t i = 0; i < nodedSegStrings.size(); i++) {
-            nodedPaths[i].reset(nodedSegStrings[i].release());
-        }
-
-        numInteriorIntersections = si.numInteriorIntersections;
-
-        if (si.hasProperInteriorIntersection()) {
-            intersectionPoint = si.getProperIntersectionPoint();
-        }
-    } else {
-        auto* arcNoder = detail::down_cast<ArcNoder*>(noder.get());
-        auto aia = std::make_unique<ArcIntersectionAdder>(cai);
-        arcNoder->setArcIntersector(std::move(aia));
-        arcNoder->computePathNodes(pathStrings);
-        nodedPaths = arcNoder->getNodedPaths();
-
-        // FIXME use actual number!
-        numInteriorIntersections = 0;
-
-
-        // numInteriorIntersections?
-        // intesectionPoint?
+    if (si.hasProperInteriorIntersection()) {
+        intersectionPoint = si.getProperIntersectionPoint();
     }
 }
 
 /* public */
 void
-IteratedNoder::computePathNodes(const std::vector<PathString*>& paths)
+IteratedNoder::computeNodes(const std::vector<SegmentString*>& segStrings)
 {
     int numInteriorIntersections;
     int nodingIterationCount = 0;
     int lastNodesCreated = -1;
-    geom::CoordinateXY intersectionPoint = geom::CoordinateXY::getNull();
+    CoordinateXY intersectionPoint = CoordinateXY::getNull();
 
     bool firstPass = true;
     do  {
         // NOTE: will change this.nodedSegStrings
         if (firstPass) {
-            node(paths, numInteriorIntersections, intersectionPoint);
+            node(segStrings, numInteriorIntersections, intersectionPoint);
             firstPass = false;
         } else {
-            auto nodingInput = PathString::toRawPointerVector(nodedPaths);
+            auto nodingInput = SegmentString::toRawPointerVector(nodedSegStrings);
             node(nodingInput, numInteriorIntersections, intersectionPoint);
         }
 

commit 35bd4a66da3dffc85ab4579e7671e3eec8fb6776
Author: Daniel Baston <dbaston at gmail.com>
Date:   Tue Jan 20 10:57:32 2026 -0500

    ArcIntersectionAdder: Accept CircularArcIntersector argument

diff --git a/include/geos/noding/ArcIntersectionAdder.h b/include/geos/noding/ArcIntersectionAdder.h
index e3e55d904..e71822df4 100644
--- a/include/geos/noding/ArcIntersectionAdder.h
+++ b/include/geos/noding/ArcIntersectionAdder.h
@@ -23,6 +23,9 @@ namespace geos::noding {
 class GEOS_DLL ArcIntersectionAdder : public ArcIntersector {
 
 public:
+    explicit ArcIntersectionAdder(algorithm::CircularArcIntersector& cai) :
+        m_intersector(cai) {}
+
     void processIntersections(ArcString& e0, std::size_t segIndex0, ArcString& e1, std::size_t segIndex1) override;
 
     void processIntersections(ArcString& e0, std::size_t segIndex0, SegmentString& e1, std::size_t segIndex1) override;
@@ -30,7 +33,7 @@ public:
     void processIntersections(SegmentString& e0, std::size_t segIndex0, SegmentString& e1, std::size_t segIndex1) override;
 
 private:
-    algorithm::CircularArcIntersector m_intersector;
+    algorithm::CircularArcIntersector& m_intersector;
 };
 
 }
\ No newline at end of file
diff --git a/include/geos/noding/IteratedNoder.h b/include/geos/noding/IteratedNoder.h
index 2af2916bc..11a0ab48f 100644
--- a/include/geos/noding/IteratedNoder.h
+++ b/include/geos/noding/IteratedNoder.h
@@ -24,6 +24,7 @@
 #include <memory>
 #include <vector>
 
+#include <geos/algorithm/CircularArcIntersector.h>
 #include <geos/algorithm/LineIntersector.h>
 #include <geos/noding/SegmentString.h> // due to inlines
 #include <geos/noding/ArcNoder.h> // for inheritance
@@ -57,6 +58,7 @@ private:
 
 
     const geom::PrecisionModel* pm;
+    algorithm::CircularArcIntersector cai;
     algorithm::LineIntersector li;
     std::vector<std::unique_ptr<PathString>> nodedPaths;
     int maxIter;
diff --git a/src/noding/IteratedNoder.cpp b/src/noding/IteratedNoder.cpp
index 746828232..df1e9beaf 100644
--- a/src/noding/IteratedNoder.cpp
+++ b/src/noding/IteratedNoder.cpp
@@ -41,6 +41,7 @@ IteratedNoder::IteratedNoder(const geom::PrecisionModel* newPm,
                              std::function<std::unique_ptr<Noder>()> noderFunction)
     :
     pm(newPm),
+    cai(pm),
     li(pm),
     maxIter(MAX_ITER),
     m_noderFunction(noderFunction)
@@ -88,8 +89,7 @@ IteratedNoder::node(const std::vector<PathString*>& pathStrings,
         }
     } else {
         auto* arcNoder = detail::down_cast<ArcNoder*>(noder.get());
-        // FIXME aia should take a PrecsionModel / LineIntersector?
-        auto aia = std::make_unique<ArcIntersectionAdder>();
+        auto aia = std::make_unique<ArcIntersectionAdder>(cai);
         arcNoder->setArcIntersector(std::move(aia));
         arcNoder->computePathNodes(pathStrings);
         nodedPaths = arcNoder->getNodedPaths();
diff --git a/tests/unit/noding/SimpleNoderTest.cpp b/tests/unit/noding/SimpleNoderTest.cpp
index 06ffb07e2..934247541 100644
--- a/tests/unit/noding/SimpleNoderTest.cpp
+++ b/tests/unit/noding/SimpleNoderTest.cpp
@@ -13,6 +13,7 @@ using geos::geom::CoordinateXY;
 using geos::geom::CoordinateSequence;
 using geos::geom::CircularArc;
 using geos::geom::Ordinate;
+using geos::algorithm::CircularArcIntersector;
 using geos::algorithm::Orientation;
 using geos::noding::ArcString;
 using geos::noding::SegmentString;
@@ -108,6 +109,7 @@ template<>
 void object::test<1>()
 {
     set_test_name("segment-segment intersection");
+    CircularArcIntersector cai;
 
     auto seq1 = std::make_shared<CoordinateSequence>();
     seq1->add(CoordinateXY{0, 0});
@@ -121,7 +123,7 @@ void object::test<1>()
 
     std::vector<PathString*> ss{&ss1, &ss2};
 
-    SimpleNoder noder(std::make_unique<geos::noding::ArcIntersectionAdder>());
+    SimpleNoder noder(std::make_unique<geos::noding::ArcIntersectionAdder>(cai));
     noder.computePathNodes(ss);
 
     auto paths = noder.getNodedPaths();
@@ -134,6 +136,7 @@ template<>
 void object::test<2>()
 {
     set_test_name("arc-arc intersection");
+    CircularArcIntersector cai;
 
     std::vector<CircularArc> arcs0;
     arcs0.push_back(makeArc(CoordinateXY{-1, 0}, {1, 0}, {0, 0}, 1, Orientation::CLOCKWISE));
@@ -146,7 +149,7 @@ void object::test<2>()
 
     std::vector<PathString*> ss{&as0, &as1};
 
-    SimpleNoder noder(std::make_unique<geos::noding::ArcIntersectionAdder>());
+    SimpleNoder noder(std::make_unique<geos::noding::ArcIntersectionAdder>(cai));
     noder.computePathNodes(ss);
 
     auto paths = noder.getNodedPaths();
@@ -159,6 +162,7 @@ template<>
 void object::test<3>()
 {
     set_test_name("arc-segment intersection");
+    CircularArcIntersector cai;
 
     std::vector<CircularArc> arcs0;
     arcs0.push_back(makeArc(CoordinateXY{-1, 0}, {1, 0}, {0, 0}, 1, Orientation::CLOCKWISE));
@@ -171,7 +175,7 @@ void object::test<3>()
 
     std::vector<PathString*> ss{&as0, &ss1};
 
-    SimpleNoder noder(std::make_unique<geos::noding::ArcIntersectionAdder>());
+    SimpleNoder noder(std::make_unique<geos::noding::ArcIntersectionAdder>(cai));
     noder.computePathNodes(ss);
 
     auto paths = noder.getNodedPaths();

commit 5e1bd575034913eb1941b9fcb303cb2be2630bb3
Author: Daniel Baston <dbaston at gmail.com>
Date:   Tue Jan 20 10:51:53 2026 -0500

    CircularArcIntersector: Accept PrecisionModel argument

diff --git a/include/geos/algorithm/CircularArcIntersector.h b/include/geos/algorithm/CircularArcIntersector.h
index 58f9aae66..8affd3c65 100644
--- a/include/geos/algorithm/CircularArcIntersector.h
+++ b/include/geos/algorithm/CircularArcIntersector.h
@@ -38,6 +38,10 @@ public:
         COCIRCULAR_INTERSECTION = 3,
     };
 
+    explicit CircularArcIntersector(const geom::PrecisionModel* pm = nullptr)
+        : precisionModel(pm)
+    {}
+
     intersection_type getResult() const
     {
         return result;
@@ -146,6 +150,7 @@ private:
 
     std::array<CoordinateXYZM, 2> intPt;
     std::array<CircularArc, 2> intArc;
+    const geom::PrecisionModel* precisionModel;
     intersection_type result = NO_INTERSECTION;
     std::uint8_t nPt = 0;
     std::uint8_t nArc = 0;
diff --git a/src/algorithm/CircularArcIntersector.cpp b/src/algorithm/CircularArcIntersector.cpp
index e17699c3e..1b3bedcaf 100644
--- a/src/algorithm/CircularArcIntersector.cpp
+++ b/src/algorithm/CircularArcIntersector.cpp
@@ -438,7 +438,7 @@ void
 CircularArcIntersector::intersects(const CoordinateSequence &p, std::size_t p0, std::size_t p1,
                                    const CoordinateSequence &q, std::size_t q0, std::size_t q1)
 {
-    LineIntersector li;
+    LineIntersector li(precisionModel);
     li.computeIntersection(p, p0, p1, q, q0, q1);
 
     if (li.getIntersectionNum() == 2) {
@@ -570,6 +570,12 @@ CircularArcIntersector::addCocircularIntersection(double startAngle, double endA
     CoordinateXYZM computedMidPt(CircularArcs::createPoint(center, radius, theta1));
     CoordinateXYZM computedEndPt(CircularArcs::createPoint(center, radius, endAngle));
 
+    if (precisionModel) {
+        precisionModel->makePrecise(computedStartPt);
+        precisionModel->makePrecise(computedMidPt);
+        precisionModel->makePrecise(computedEndPt);
+    }
+
     // Check to see if the endpoints of the intersection match the endpoints of either of
     // the input arcs. Use angles for the check to avoid missing an endpoint intersection from
     // inaccuracy in the point construction.
@@ -610,6 +616,10 @@ CircularArcIntersector::addArcArcIntersectionPoint(const CoordinateXY& computedI
     CoordinateXYZM& newIntPt = intPt[nPt++];
     newIntPt = computedIntPt;
 
+    if (precisionModel) {
+        precisionModel->makePrecise(newIntPt);
+    }
+
     if (computedIntPt.equals2D(arc1.p0())) {
         arc1.applyAt(0, [&newIntPt](const auto& endpoint) {
             newIntPt.z = Interpolate::zGet(newIntPt, endpoint);
@@ -632,6 +642,10 @@ CircularArcIntersector::addArcSegmentIntersectionPoint(const CoordinateXY& compu
     CoordinateXYZM& newIntPt = intPt[nPt++];
     newIntPt = computedIntPt;
 
+    if (precisionModel) {
+        precisionModel->makePrecise(newIntPt);
+    }
+
     for (int i = 0; i < 2; i++) {
         if (useSegEndpoints) {
             if (computedIntPt.equals2D(seq.getAt<CoordinateXY>(pos0))) {
diff --git a/tests/unit/algorithm/CircularArcIntersectorTest.cpp b/tests/unit/algorithm/CircularArcIntersectorTest.cpp
index dc07a91aa..6ad649d02 100644
--- a/tests/unit/algorithm/CircularArcIntersectorTest.cpp
+++ b/tests/unit/algorithm/CircularArcIntersectorTest.cpp
@@ -16,6 +16,7 @@ using geos::geom::CoordinateXY;
 using geos::geom::CoordinateXYM;
 using geos::geom::CoordinateXYZM;
 using geos::geom::CircularArc;
+using geos::geom::PrecisionModel;
 using geos::MATH_PI;
 
 namespace tut {
@@ -1612,6 +1613,86 @@ void object::test<81>
     );
 }
 
+template<>
+template<>
+void object::test<82>()
+{
+    set_test_name("arc/segment interior intersection with fixed PrecisionModel");
+
+    PrecisionModel pm(10);
+    CircularArcIntersector cai(&pm);
+
+    auto arcPts = CoordinateSequence::XY(3);
+    arcPts.setAt(XY{-200, 0}, 0);
+    arcPts.setAt(XY{0, 200}, 1);
+    arcPts.setAt(XY{200, 0}, 2);
+    CircularArc arc(arcPts, 0);
+
+    auto segPts = CoordinateSequence::XY(2);
+    segPts.setAt(XY{0, 0}, 0);
+    segPts.setAt(XY{200, 200}, 0);
+
+    cai.intersects(arc, segPts, 0, 1, false);
+
+    ensure_equals(cai.getNumPoints(), 1u);
+    ensure_equals(cai.getPoint(0), XY{141.4, 141.4});
+}
+
+template<>
+template<>
+void object::test<83>()
+{
+    set_test_name("arc/arc interior intersection with fixed PrecisionModel");
+
+    PrecisionModel pm(10);
+    CircularArcIntersector cai(&pm);
+
+    auto arcPts1 = CoordinateSequence::XY(3);
+    arcPts1.setAt(XY{-200, 0}, 0);
+    arcPts1.setAt(XY{0, 200}, 1);
+    arcPts1.setAt(XY{200, 0}, 2);
+    CircularArc arc1(arcPts1, 0);
+
+    auto arcPts2 = CoordinateSequence::XY(3);
+    arcPts2.setAt(XY{0, 0}, 0);
+    arcPts2.setAt(XY{200, 200}, 1);
+    arcPts2.setAt(XY{400, 0}, 2);
+    CircularArc arc2(arcPts2, 0);
+
+    cai.intersects(arc1, arc2);
+
+    ensure_equals(cai.getNumPoints(), 1u);
+    ensure_equals(cai.getPoint(0), XY{100, 173.2});
+}
+
+template<>
+template<>
+void object::test<84>()
+{
+    set_test_name("degenerate arc/degenerate arc intersection with fixed PrecisionModel");
+
+    PrecisionModel pm(10);
+    CircularArcIntersector cai(&pm);
+
+    auto arcPts1 = CoordinateSequence::XY(3);
+    arcPts1.setAt(XY{0, 0}, 0);
+    arcPts1.setAt(XY{35, 75}, 1);
+    arcPts1.setAt(XY{70, 150}, 2);
+    CircularArc arc1(arcPts1, 0);
+
+    auto arcPts2 = CoordinateSequence::XY(3);
+    arcPts2.setAt(XY{0, 100}, 0);
+    arcPts2.setAt(XY{50, 50}, 1);
+    arcPts2.setAt(XY{100, 0}, 2);
+    CircularArc arc2(arcPts2, 0);
+
+    cai.intersects(arc1, arc2);
+
+    ensure_equals(cai.getNumPoints(), 1u);
+    ensure_equals(cai.getPoint(0), XY{31.8, 68.2});
+}
+
+
 // TODO: check Z values of arc result centerpoints
 // TODO: add tests for seg/seg
 

commit 15ce6735d6ba3014fc12fcf86df9eecf92db9739
Author: Daniel Baston <dbaston at gmail.com>
Date:   Mon Jan 19 16:56:54 2026 -0500

    GeometryNoder: Readability improvements

diff --git a/src/noding/GeometryNoder.cpp b/src/noding/GeometryNoder.cpp
index 81e759886..6103364ea 100644
--- a/src/noding/GeometryNoder.cpp
+++ b/src/noding/GeometryNoder.cpp
@@ -65,7 +65,6 @@ public:
     {
         if(const auto* ls = dynamic_cast<const geom::LineString*>(g)) {
             auto coord = ls->getSharedCoordinates();
-            // coord ownership transferred to SegmentString
             auto ss = std::make_unique<NodedSegmentString>(coord, _constructZ, _constructM, nullptr);
             _to.push_back(std::move(ss));
         } else if (const auto* cs = dynamic_cast<const geom::CircularString*>(g)) {
@@ -119,19 +118,17 @@ GeometryNoder::toGeometry(std::vector<std::unique_ptr<PathString>>& nodedEdges)
 
     bool resultArcs = false;
     for(auto& path :  nodedEdges) {
-        if (const auto* ss = dynamic_cast<SegmentString*>(path.get())) {
-            const auto& coords = ss->getCoordinates();
+        const auto& coords = path->getCoordinates();
 
-            // Check if an equivalent edge is known
-            OrientedCoordinateArray oca1(*coords);
-            if(ocas.insert(oca1).second) {
+        OrientedCoordinateArray oca1(*coords);
+        // Check if an equivalent edge is known
+        if(ocas.insert(oca1).second) {
+            if (dynamic_cast<SegmentString*>(path.get())) {
                 lines.push_back(geomFact->createLineString(coords));
+            } else {
+                resultArcs = true;
+                lines.push_back(geomFact->createCircularString(coords));
             }
-        } else {
-            resultArcs = true;
-            auto* as = dynamic_cast<ArcString*>(path.get());
-            // FIXME: check for duplicates
-            lines.push_back(geomFact->createCircularString(as->getCoordinates()));
         }
     }
 

commit f54ead7568472702e67563ee1d559b5d7c6d6b66
Author: Daniel Baston <dbaston at gmail.com>
Date:   Mon Jan 19 16:48:01 2026 -0500

    CircularString: Store arcs as a member variable

diff --git a/include/geos/geom/CircularString.h b/include/geos/geom/CircularString.h
index 6a6d5f8c3..e38a5b640 100644
--- a/include/geos/geom/CircularString.h
+++ b/include/geos/geom/CircularString.h
@@ -14,6 +14,7 @@
 
 #pragma once
 
+#include <geos/geom/CircularArc.h>
 #include <geos/geom/SimpleCurve.h>
 
 namespace geos {
@@ -30,6 +31,8 @@ public:
 
     std::unique_ptr<CircularString> clone() const;
 
+    const std::vector<CircularArc>& getArcs() const;
+
     std::string getGeometryType() const override;
 
     GeometryTypeId getGeometryTypeId() const override;
@@ -81,6 +84,11 @@ protected:
 
     void validateConstruction();
 
+private:
+    void createArcs() const;
+
+    mutable std::vector<CircularArc> arcs;
+
 };
 
 
diff --git a/src/algorithm/Area.cpp b/src/algorithm/Area.cpp
index 94889933d..1311ebd1f 100644
--- a/src/algorithm/Area.cpp
+++ b/src/algorithm/Area.cpp
@@ -21,8 +21,10 @@
 
 #include <geos/algorithm/Area.h>
 #include <geos/geom/CircularArc.h>
+#include <geos/geom/CircularString.h>
 #include <geos/geom/Curve.h>
 #include <geos/geom/SimpleCurve.h>
+#include <geos/util.h>
 #include <geos/util/IllegalArgumentException.h>
 
 using geos::geom::CoordinateXY;
@@ -128,22 +130,22 @@ Area::ofClosedCurve(const geom::Curve& ring) {
         const geom::CoordinateSequence& coords = *section.getCoordinatesRO();
 
         if (section.isCurved()) {
-            for (std::size_t j = 2; j < coords.size(); j += 2) {
-                const CoordinateXY& p0 = coords.getAt<CoordinateXY>(j-2);
-                const CoordinateXY& p1 = coords.getAt<CoordinateXY>(j-1);
-                const CoordinateXY& p2 = coords.getAt<CoordinateXY>(j);
+            const auto* cs = detail::down_cast<const geom::CircularString*>(&section);
+
+            for (const auto& arc : cs->getArcs()) {
+                const CoordinateXY& p0 = arc.p0();
+                const CoordinateXY& p2 = arc.p2();
 
                 double triangleArea = 0.5*(p0.x*p2.y - p2.x*p0.y);
                 sum += triangleArea;
 
-                geom::CircularArc arc(coords, j-2);
                 if (arc.isLinear()) {
                     continue;
                 }
 
                 double circularSegmentArea = arc.getArea();
 
-                if (algorithm::Orientation::index(p0, p2, p1) == algorithm::Orientation::CLOCKWISE) {
+                if (arc.isCCW()) {
                     sum += circularSegmentArea;
                 } else {
                     sum -= circularSegmentArea;
diff --git a/src/geom/CircularArc.cpp b/src/geom/CircularArc.cpp
index eb2ffe5df..9b5476953 100644
--- a/src/geom/CircularArc.cpp
+++ b/src/geom/CircularArc.cpp
@@ -381,17 +381,6 @@ CircularArc::equals(const CircularArc &other, double tol) const
     return true;
 }
 
-
-#if 0
-std::pair<CircularArc, CircularArc>
-CircularArc::splitAtPoint(const CoordinateXY& q) const {
-    return std::make_pair(
-        CircularArc(p0(), q, getCenter(), getRadius(), getOrientation()),
-        CircularArc(q, p2(), getCenter(), getRadius(), getOrientation())
-    );
-}
-#endif
-
 std::string
 CircularArc::toString() const {
     std::stringstream ss;
diff --git a/src/geom/CircularString.cpp b/src/geom/CircularString.cpp
index 361f56c56..ac5311905 100644
--- a/src/geom/CircularString.cpp
+++ b/src/geom/CircularString.cpp
@@ -46,6 +46,23 @@ CircularString::clone() const
     return std::unique_ptr<CircularString>(cloneImpl());
 }
 
+void
+CircularString::createArcs() const
+{
+    for (std::size_t i = 0; i < points->getSize() - 2; i += 2) {
+        arcs.emplace_back(*points, i);
+    }
+}
+
+const std::vector<CircularArc>&
+CircularString::getArcs() const
+{
+    if (arcs.empty()) {
+        createArcs();
+    }
+    return arcs;
+}
+
 std::string
 CircularString::getGeometryType() const
 {
@@ -65,13 +82,11 @@ CircularString::getLength() const
         return 0;
     }
 
-    const CoordinateSequence& coords = *getCoordinatesRO();
-
     double tot = 0;
-    for (std::size_t i = 2; i < coords.size(); i += 2) {
-        auto len = CircularArc(coords, i-2).getLength();
-        tot += len;
+    for (const auto& arc : getArcs()) {
+        tot += arc.getLength();
     }
+
     return tot;
 }
 
diff --git a/src/noding/GeometryNoder.cpp b/src/noding/GeometryNoder.cpp
index 6103a7407..81e759886 100644
--- a/src/noding/GeometryNoder.cpp
+++ b/src/noding/GeometryNoder.cpp
@@ -48,7 +48,7 @@ namespace noding { // geos.noding
 namespace {
 
 /**
- * Add every linear element in a geometry into SegmentString vector
+ * Add every linear element in a geometry into PathString vector
  */
 class PathStringExtractor: public geom::GeometryComponentFilter {
 public:
@@ -70,12 +70,7 @@ public:
             _to.push_back(std::move(ss));
         } else if (const auto* cs = dynamic_cast<const geom::CircularString*>(g)) {
             const auto& coords = cs->getSharedCoordinates();
-
-            // TODO: Store this vector in the CircularString ?
-            std::vector<geom::CircularArc> arcs;
-            for (std::size_t i = 0; i < coords->getSize() - 2; i += 2) {
-                arcs.emplace_back(*coords, i);
-            }
+            auto arcs = cs->getArcs();
 
             auto as = std::make_unique<NodableArcString>(std::move(arcs), coords, _constructZ, _constructM, nullptr);
             _to.push_back(std::move(as));

commit bd03a4b050c553f9c6b84ed1554abe5464cd3e20
Author: Daniel Baston <dbaston at gmail.com>
Date:   Mon Jan 19 16:29:39 2026 -0500

    NodableArcString: construct from std::shared_ptr<const CoordinateSequence>

diff --git a/include/geos/noding/ArcString.h b/include/geos/noding/ArcString.h
index 67285f33c..e758b7307 100644
--- a/include/geos/noding/ArcString.h
+++ b/include/geos/noding/ArcString.h
@@ -32,9 +32,9 @@ public:
     explicit ArcString(std::vector<geom::CircularArc> arcs) : m_arcs(std::move(arcs)) {
     }
 
-    ArcString(std::vector<geom::CircularArc> arcs, std::unique_ptr<geom::CoordinateSequence> seq, void* context)
+    ArcString(std::vector<geom::CircularArc> arcs, const std::shared_ptr<const geom::CoordinateSequence>& seq, void* context)
         : m_arcs(std::move(arcs)),
-          m_seq(std::move(seq)),
+          m_seq(seq),
           m_context(context)
     {}
 
diff --git a/include/geos/noding/NodableArcString.h b/include/geos/noding/NodableArcString.h
index 2fcd5be2d..b7e438f8f 100644
--- a/include/geos/noding/NodableArcString.h
+++ b/include/geos/noding/NodableArcString.h
@@ -23,7 +23,7 @@ namespace geos::noding {
 class GEOS_DLL NodableArcString : public ArcString, public NodablePath {
 
 public:
-    NodableArcString(std::vector<geom::CircularArc> arcs, std::unique_ptr<geom::CoordinateSequence> coords, bool constructZ, bool constructM, void* context);
+    NodableArcString(std::vector<geom::CircularArc> arcs, const std::shared_ptr<const geom::CoordinateSequence>& coords, bool constructZ, bool constructM, void* context);
 
     std::unique_ptr<ArcString> clone() const;
 
diff --git a/src/noding/GeometryNoder.cpp b/src/noding/GeometryNoder.cpp
index d65676f76..6103a7407 100644
--- a/src/noding/GeometryNoder.cpp
+++ b/src/noding/GeometryNoder.cpp
@@ -69,7 +69,7 @@ public:
             auto ss = std::make_unique<NodedSegmentString>(coord, _constructZ, _constructM, nullptr);
             _to.push_back(std::move(ss));
         } else if (const auto* cs = dynamic_cast<const geom::CircularString*>(g)) {
-            auto coords = cs->getCoordinates();
+            const auto& coords = cs->getSharedCoordinates();
 
             // TODO: Store this vector in the CircularString ?
             std::vector<geom::CircularArc> arcs;
@@ -77,7 +77,7 @@ public:
                 arcs.emplace_back(*coords, i);
             }
 
-            auto as = std::make_unique<NodableArcString>(std::move(arcs), std::move(coords), _constructZ, _constructM, nullptr);
+            auto as = std::make_unique<NodableArcString>(std::move(arcs), coords, _constructZ, _constructM, nullptr);
             _to.push_back(std::move(as));
         }
     }
diff --git a/src/noding/NodableArcString.cpp b/src/noding/NodableArcString.cpp
index 96d8e8880..35969cac5 100644
--- a/src/noding/NodableArcString.cpp
+++ b/src/noding/NodableArcString.cpp
@@ -29,8 +29,8 @@ pseudoAngleDiffCCW(double paStart, double pa) {
     return diff;
 }
 
-NodableArcString::NodableArcString(std::vector<geom::CircularArc> arcs, std::unique_ptr<geom::CoordinateSequence> coords, bool constructZ, bool constructM, void* context) :
-    ArcString(std::move(arcs), std::move(coords), context),
+NodableArcString::NodableArcString(std::vector<geom::CircularArc> arcs, const std::shared_ptr<const geom::CoordinateSequence>& coords, bool constructZ, bool constructM, void* context) :
+    ArcString(std::move(arcs), coords, context),
     m_constructZ(constructZ),
     m_constructM(constructM)
 {

commit 78881d3917a5b22e916384c496ed15c931acc0f8
Author: Daniel Baston <dbaston at gmail.com>
Date:   Mon Jan 19 16:23:01 2026 -0500

    CircularArcIntersector: Readability improvements

diff --git a/include/geos/algorithm/Angle.h b/include/geos/algorithm/Angle.h
index 192891022..ee786179f 100644
--- a/include/geos/algorithm/Angle.h
+++ b/include/geos/algorithm/Angle.h
@@ -228,6 +228,25 @@ public:
     /// @return true if `angle` is within [from, to]
     static bool isWithinCCW(double angle, double from, double to);
 
+    /// Return which of the two provided angles would be encountered first when moving
+    /// counterclockwise from the specified start angle.
+    ///
+    /// @param from the starting angle
+    /// @param a the first candidate angle
+    /// @param b the second candidate angle
+    ///
+    /// @return a or b
+    static double nextCCW(double from, double a, double b);
+
+    /// Return the fraction of an angle as a fraction of a larger angle
+    ///
+    /// @param x an angle between a and b
+    /// @param a the starting angle
+    /// @param b the ending angle
+    ///
+    /// @return a value in the range [0, 1]
+    static double fractionCCW(double x, double a, double b);
+
     /// Computes the unoriented smallest difference between two angles.
     ///
     /// The angles are assumed to be normalized to the range [-Pi, Pi].
diff --git a/include/geos/algorithm/CircularArcIntersector.h b/include/geos/algorithm/CircularArcIntersector.h
index 80e1543ab..58f9aae66 100644
--- a/include/geos/algorithm/CircularArcIntersector.h
+++ b/include/geos/algorithm/CircularArcIntersector.h
@@ -103,7 +103,7 @@ private:
      * When the endpoints of the new arc correspond with those of the inputs, Z/M values
      * will be preferentially taken from arc1.
      */
-    void addArcIntersection(double startAngle, double endAngle, int orientation, const CircularArc& arc1, const CircularArc& arc2);
+    void addCocircularIntersection(double startAngle, double endAngle, int orientation, const CircularArc& arc1, const CircularArc& arc2);
 
     /** Add a point intersection between two arcs.
      *
@@ -113,7 +113,7 @@ private:
      * If the intersection point does not equal the endpoint of either arc, its Z/M values
      * will be interpolated.
      */
-    void addIntersection(const CoordinateXY& computedIntPt, const CircularArc& arc1, const CircularArc& arc2);
+    void addArcArcIntersectionPoint(const CoordinateXY& computedIntPt, const CircularArc& arc1, const CircularArc& arc2);
 
     /** Add a point intersection between an arc and a segment.
      *
@@ -123,7 +123,8 @@ private:
      * `useSegEndpoint` is true. If the intersection point does not equal the endpoint of the arc
      * or the segment, its Z/M values will be interpolated.
      */
-    void addIntersection(const CoordinateXY& computedIntPt, const CircularArc& lhs, const geom::CoordinateSequence& seq, std::size_t pos0, std::size_t pos1, bool useSegEndpoints);
+    void addArcSegmentIntersectionPoint(const CoordinateXY& computedIntPt, const CircularArc& lhs,
+        const geom::CoordinateSequence& seq, std::size_t pos0, std::size_t pos1, bool useSegEndpoints);
 
     /** Determines whether and where two circles intersect a line segment.
      *
@@ -140,6 +141,9 @@ private:
 
     void computeCocircularIntersection(const CircularArc& arc1, const CircularArc& arc2);
 
+    /** Checks whether the provided point has already been recorded as an intersection point. */
+    bool hasIntersection(const geom::CoordinateXY& p) const;
+
     std::array<CoordinateXYZM, 2> intPt;
     std::array<CircularArc, 2> intArc;
     intersection_type result = NO_INTERSECTION;
diff --git a/src/algorithm/Angle.cpp b/src/algorithm/Angle.cpp
index b63dc4db8..e372a4a9a 100644
--- a/src/algorithm/Angle.cpp
+++ b/src/algorithm/Angle.cpp
@@ -156,6 +156,29 @@ Angle::getTurn(double ang1, double ang2)
     return NONE;
 }
 
+double
+Angle::nextCCW(double from, double a, double b)
+{
+    if (normalizePositive(a - from) < normalizePositive(b - from)) {
+        return a;
+    }
+
+    return b;
+}
+
+double
+Angle::fractionCCW(double x, double a, double b)
+{
+    if (x < a) {
+        x += 2*MATH_PI;
+    }
+    if (b < a) {
+        b += 2*MATH_PI;
+    }
+    return (x - a) / (b - a);
+}
+
+
 /* public static */
 double
 Angle::normalize(double angle)
diff --git a/src/algorithm/CircularArcIntersector.cpp b/src/algorithm/CircularArcIntersector.cpp
index ba0ab0ccd..e17699c3e 100644
--- a/src/algorithm/CircularArcIntersector.cpp
+++ b/src/algorithm/CircularArcIntersector.cpp
@@ -3,7 +3,7 @@
  * GEOS - Geometry Engine Open Source
  * http://geos.osgeo.org
  *
- * Copyright (C) 2024-2025 ISciences, LLC
+ * Copyright (C) 2024-2026 ISciences, LLC
  *
  * This is free software; you can redistribute and/or modify it under
  * the terms of the GNU Lesser General Public Licence as published
@@ -25,29 +25,6 @@ using geos::geom::CircularArc;
 
 namespace geos::algorithm {
 
-static double
-nextAngleCCW(double from, double a, double b)
-{
-    if (Angle::normalizePositive(a - from) < Angle::normalizePositive(b - from)) {
-        return a;
-    }
-    else {
-        return b;
-    }
-}
-
-static double
-angleFractionCCW(double x, double a, double b)
-{
-    if (x < a) {
-        x += 2*MATH_PI;
-    }
-    if (b < a) {
-        b += 2*MATH_PI;
-    }
-    return (x - a) / (b - a);
-}
-
 static double
 interpolateValue(double a1, double a2, double frac)
 {
@@ -61,53 +38,50 @@ interpolateValue(double a1, double a2, double frac)
     return a1 + frac * (a2 - a1);
 }
 
-static void interpolateZM(const CircularArc& arc,
-                   const CoordinateXY& pt,
-                   double& z, double& m)
+static void
+interpolateZM(const CircularArc& arc, const CoordinateXY& pt, double& z, double& m)
 {
-    using geos::geom::Ordinate;
+    using geom::Ordinate;
 
-    const geom::CoordinateSequence& seq = *arc.getCoordinateSequence();
+    const CoordinateSequence& seq = *arc.getCoordinateSequence();
     std::size_t i0 = arc.getCoordinatePosition();
 
-    if (arc.p1().equals2D(pt)) {
-        seq.applyAt(i0 + 1, [&z, &m](const auto& arcPt) {
-            z = arcPt.template get<Ordinate::Z>();
-            m = arcPt.template get<Ordinate::M>();
-        });
-        return;
-    }
-
-    double z0, m0;
-    seq.applyAt(i0, [&z0, &m0](const auto& arcPt) {
-        z0 = arcPt.template get<Ordinate::Z>();
-        m0 = arcPt.template get<Ordinate::M>();
-    });
-    if (arc.p0().equals2D(pt)) {
-        z = z0;
-        m = m0;
-        return;
-    }
-
+    // Read Z, M from control point
     double z1, m1;
     seq.applyAt(i0 + 1, [&z1, &m1](const auto& arcPt) {
         z1 = arcPt.template get<Ordinate::Z>();
         m1 = arcPt.template get<Ordinate::M>();
     });
-
+    // Test point = control point?
+    // Take Z, M from the control point
     if (arc.p1().equals2D(pt)) {
         z = z1;
         m = m1;
         return;
     }
 
+    // Read Z, M from start point
+    double z0, m0;
+    seq.applyAt(i0, [&z0, &m0](const auto& arcPt) {
+        z0 = arcPt.template get<Ordinate::Z>();
+        m0 = arcPt.template get<Ordinate::M>();
+    });
+    // Test point = start point?
+    // Take Z, M from the start point
+    if (arc.p0().equals2D(pt)) {
+        z = z0;
+        m = m0;
+        return;
+    }
 
+    // Read Z, M from end point
     double z2, m2;
     seq.applyAt(i0 + 2, [&z2, &m2](const auto& arcPt) {
         z2 = arcPt.template get<Ordinate::Z>();
         m2 = arcPt.template get<Ordinate::M>();
     });
-
+    // Test point = end point?
+    // Take Z, M from the end point
     if (arc.p2().equals2D(pt)) {
         z = z2;
         m = m2;
@@ -127,37 +101,39 @@ static void interpolateZM(const CircularArc& arc,
 
     if (std::isnan(z1)) {
         // Interpolate between p0 /  p2
-        const double frac = angleFractionCCW(theta, theta0, theta2);
+        const double frac = Angle::fractionCCW(theta, theta0, theta2);
         z = interpolateValue(z0, z2, frac);
     } else if (Angle::isWithinCCW(theta, theta0, theta1)) {
         // Interpolate between p0 / p1
-        const double frac = angleFractionCCW(theta, theta0, theta1);
+        const double frac = Angle::fractionCCW(theta, theta0, theta1);
         z = interpolateValue(z0, z1, frac);
     } else {
         // Interpolate between p1 / p2
-        const double frac = angleFractionCCW(theta, theta1, theta2);
+        const double frac = Angle::fractionCCW(theta, theta1, theta2);
         z = interpolateValue(z1, z2, frac);
     }
 
     if (std::isnan(m1)) {
         // Interpolate between p0 /  p2
-        const double frac = angleFractionCCW(theta, theta0, theta2);
+        const double frac = Angle::fractionCCW(theta, theta0, theta2);
         m = interpolateValue(m0, m2, frac);
     } else if (Angle::isWithinCCW(theta, theta0, theta1)) {
         // Interpolate between p0 / p1
-        const double frac = angleFractionCCW(theta, theta0, theta1);
+        const double frac = Angle::fractionCCW(theta, theta0, theta1);
         m = interpolateValue(m0, m1, frac);
     } else {
         // Interpolate between p1 / p2
-        const double frac = angleFractionCCW(theta, theta1, theta2);
+        const double frac = Angle::fractionCCW(theta, theta1, theta2);
         m = interpolateValue(m1, m2, frac);
     }
 
 }
 
-static void interpolateSegmentZM(const CoordinateSequence& seq,
-                              std::size_t ind0, std::size_t ind1,
-                              geom::CoordinateXY& pt, double& z, double& m)
+// Interpolate the Z/M values of a point lying on the provided line segment
+static void
+interpolateSegmentZM(const CoordinateSequence& seq,
+                     std::size_t ind0, std::size_t ind1,
+                     CoordinateXY& pt, double& z, double& m)
 {
     seq.applyAt(ind0, [&seq, &pt, ind1, &z, &m](const auto& p0) {
         using CoordinateType = std::decay_t<decltype(p0)>;
@@ -169,7 +145,9 @@ static void interpolateSegmentZM(const CoordinateSequence& seq,
 }
 
 
-static void interpolateZM(const CircularArc& arc0, const CircularArc& arc1, geom::CoordinateXYZM& pt)
+// Interpolate the Z/M values of an intersection point between two arcs
+static void
+interpolateZM(const CircularArc& arc0, const CircularArc& arc1, geom::CoordinateXYZM& pt)
 {
     if (!std::isnan(pt.z) && !std::isnan(pt.m)) {
         return;
@@ -188,10 +166,12 @@ static void interpolateZM(const CircularArc& arc0, const CircularArc& arc1, geom
     }
 }
 
-static void interpolateZM(const CircularArc& arc0,
-                       const geom::CoordinateSequence& seq,
-                       std::size_t ind0, std::size_t ind1,
-                       geom::CoordinateXYZM& pt)
+// Interpolate the Z/M values of an intersection point between an arc and a segment
+static void
+interpolateZM(const CircularArc& arc0,
+              const CoordinateSequence& seq,
+              std::size_t ind0, std::size_t ind1,
+              geom::CoordinateXYZM& pt)
 {
     if (!std::isnan(pt.z) && !std::isnan(pt.m)) {
         return;
@@ -277,6 +257,19 @@ CircularArcIntersector::circleIntersects(const CoordinateXY& center,
     return n;
 }
 
+bool
+CircularArcIntersector::hasIntersection(const geom::CoordinateXY &p) const {
+    switch (nPt) {
+        case 2: return intPt[1].equals2D(p) || intPt[0].equals2D(p);
+        case 1: return intPt[0].equals2D(p);
+        case 0: return false;
+        default: break;
+    }
+
+    assert(0);
+    return false;
+}
+
 void
 CircularArcIntersector::intersects(const CircularArc& arc, const CoordinateSequence& seq, std::size_t segPos0, std::size_t segPos1, bool useSegEndpoints)
 {
@@ -297,11 +290,11 @@ CircularArcIntersector::intersects(const CircularArc& arc, const CoordinateSeque
     auto n = circleIntersects(c, r, seq.getAt<CoordinateXY>(segPos0), seq.getAt<CoordinateXY>(segPos1), isect0, isect1);
 
     if (n > 0 && arc.containsPointOnCircle(isect0)) {
-        addIntersection(isect0, arc, seq, segPos0, segPos1, useSegEndpoints);
+        addArcSegmentIntersectionPoint(isect0, arc, seq, segPos0, segPos1, useSegEndpoints);
     }
 
     if (n > 1  && arc.containsPointOnCircle(isect1)) {
-        addIntersection(isect1, arc, seq, segPos0, segPos1, useSegEndpoints);
+        addArcSegmentIntersectionPoint(isect1, arc, seq, segPos0, segPos1, useSegEndpoints);
     }
 
     switch (nPt) {
@@ -370,17 +363,17 @@ CircularArcIntersector::intersects(const CircularArc& arc1, const CircularArc& a
         computeCocircularIntersection(arc1, arc2);
     } else {
         // Explicitly add endpoint intersections that may be missed or inexactly computed.
-        if (nPt < 2 && arc1.p0().equals2D(arc2.p0()) && (nPt == 0 || !intPt[0].equals2D(arc1.p0()))) {
-            addIntersection(arc1.p0(), arc1, arc2);
+        if (arc1.p0().equals2D(arc2.p0()) && !hasIntersection(arc1.p0())) {
+            addArcArcIntersectionPoint(arc1.p0(), arc1, arc2);
         }
-        if (nPt < 2 && arc1.p0().equals2D(arc2.p2()) && (nPt == 0 || !intPt[0].equals2D(arc1.p0()))) {
-            addIntersection(arc1.p0(), arc1, arc2);
+        if (arc1.p0().equals2D(arc2.p2()) && !hasIntersection(arc1.p0())) {
+            addArcArcIntersectionPoint(arc1.p0(), arc1, arc2);
         }
-        if (nPt < 2 && arc1.p2().equals2D(arc2.p0()) && (nPt == 0 || !intPt[0].equals2D(arc1.p2()))) {
-            addIntersection(arc1.p2(), arc1, arc2);
+        if (arc1.p2().equals2D(arc2.p0()) && !hasIntersection(arc1.p2())) {
+            addArcArcIntersectionPoint(arc1.p2(), arc1, arc2);
         }
-        if (nPt < 2 && arc1.p2().equals2D(arc2.p2()) && (nPt == 0 || !intPt[0].equals2D(arc1.p2()))) {
-            addIntersection(arc1.p2(), arc1, arc2);
+        if (arc1.p2().equals2D(arc2.p2()) && !hasIntersection(arc1.p2())) {
+            addArcArcIntersectionPoint(arc1.p2(), arc1, arc2);
         }
 
         if (nPt < 2) {
@@ -399,8 +392,8 @@ CircularArcIntersector::intersects(const CircularArc& arc1, const CircularArc& a
             CoordinateXY isect1{p.x - h* dy/d, p.y + h* dx/d };
 
             // One of the computed intersection points may be an inexact version of an endpoint.
-            // If we already have an endpoint intersection, process the farther-away computed
-            // point first.
+            // If we already have an endpoint intersection, we need to process the farther-away
+            // computed point first.
             if (nPt == 1 && intPt[0].distance(isect0) < intPt[0].distance(isect1)) {
                 std::swap(isect0, isect1);
             }
@@ -415,7 +408,7 @@ CircularArcIntersector::intersects(const CircularArc& arc1, const CircularArc& a
                 }
 
                 if (arc1.containsPointOnCircle(computedIntPt) && arc2.containsPointOnCircle(computedIntPt)) {
-                    addIntersection(computedIntPt, arc1, arc2);
+                    addArcArcIntersectionPoint(computedIntPt, arc1, arc2);
                 }
             }
         }
@@ -435,6 +428,8 @@ CircularArcIntersector::intersects(const CircularArc& arc1, const CircularArc& a
         case 0:
             result = NO_INTERSECTION;
             break;
+        default:
+            assert(0);
         }
     }
 }
@@ -460,6 +455,7 @@ CircularArcIntersector::intersects(const CoordinateSequence &p, std::size_t p0,
     }
 }
 
+/// Overwrite X/Y, and NaN Z/M values on the supplied point with those from the coordinate at the specified index
 static void
 setFromEndpoint(geom::CoordinateXYZM& pt, const CircularArc& arc, std::size_t index)
 {
@@ -510,7 +506,7 @@ CircularArcIntersector::computeCocircularIntersection(const CircularArc& arc1, c
         if (Angle::isWithinCCW(bp0, ap0, ap1)) {
             checkAcontained = false;
             const double start = bp0;
-            const double end = nextAngleCCW(start, bp1, ap1);
+            const double end = Angle::nextCCW(start, bp1, ap1);
 
             if (end == bp1) {
                 checkBp1inA = false;
@@ -518,14 +514,14 @@ CircularArcIntersector::computeCocircularIntersection(const CircularArc& arc1, c
 
             if (start == end) {
                 const CoordinateXY computedIntPt = CircularArcs::createPoint(center, radius, start);
-                addIntersection(computedIntPt, arc1, arc2);
+                addArcArcIntersectionPoint(computedIntPt, arc1, arc2);
             }
             else {
                 if (resultArcIsCCW) {
-                    addArcIntersection(start, end, Orientation::COUNTERCLOCKWISE, arc1, arc2);
+                    addCocircularIntersection(start, end, Orientation::COUNTERCLOCKWISE, arc1, arc2);
                 }
                 else {
-                    addArcIntersection(end, start, Orientation::CLOCKWISE, arc1, arc2);
+                    addCocircularIntersection(end, start, Orientation::CLOCKWISE, arc1, arc2);
                 }
             }
         }
@@ -538,30 +534,30 @@ CircularArcIntersector::computeCocircularIntersection(const CircularArc& arc1, c
             const double end = bp1;
             if (start == end) {
                 const CoordinateXY computedIntPt = CircularArcs::createPoint(center, radius, start);
-                addIntersection(computedIntPt, arc1, arc2);
+                addArcArcIntersectionPoint(computedIntPt, arc1, arc2);
             }
             else {
                 if (resultArcIsCCW) {
-                    addArcIntersection(start, end, Orientation::CLOCKWISE, arc1, arc2);
+                    addCocircularIntersection(start, end, Orientation::CLOCKWISE, arc1, arc2);
                 }
                 else {
-                    addArcIntersection(end, start, Orientation::CLOCKWISE, arc1, arc2);
+                    addCocircularIntersection(end, start, Orientation::CLOCKWISE, arc1, arc2);
                 }
             }
         }
 
         if (checkAcontained && Angle::isWithinCCW(ap0, bp0 , bp1) && ap0 != bp0  && ap0 != bp1 && Angle::isWithinCCW(ap1, bp0, bp1) && ap1 != bp1 && ap1 != bp0) {
             if (resultArcIsCCW) {
-                addArcIntersection(ap0, ap1, Orientation::COUNTERCLOCKWISE, arc1, arc2);
+                addCocircularIntersection(ap0, ap1, Orientation::COUNTERCLOCKWISE, arc1, arc2);
             }
             else {
-                addArcIntersection(ap1, ap0, Orientation::CLOCKWISE, arc1, arc2);
+                addCocircularIntersection(ap1, ap0, Orientation::CLOCKWISE, arc1, arc2);
             }
         }
 }
 
 void
-CircularArcIntersector::addArcIntersection(double startAngle, double endAngle, int orientation, const CircularArc& arc1, const CircularArc& arc2)
+CircularArcIntersector::addCocircularIntersection(double startAngle, double endAngle, int orientation, const CircularArc& arc1, const CircularArc& arc2)
 {
     const auto theta1  = CircularArcs::getMidpointAngle(startAngle, endAngle, orientation == Orientation::COUNTERCLOCKWISE);
     const CoordinateXY& center = arc1.getCenter();
@@ -575,8 +571,8 @@ CircularArcIntersector::addArcIntersection(double startAngle, double endAngle, i
     CoordinateXYZM computedEndPt(CircularArcs::createPoint(center, radius, endAngle));
 
     // Check to see if the endpoints of the intersection match the endpoints of either of
-    // the endpoints. Use angles for the check to avoid missing an endpoint intersection from
-    // inaccuracy the point construction.
+    // the input arcs. Use angles for the check to avoid missing an endpoint intersection from
+    // inaccuracy in the point construction.
     if (startAngle == arc1.theta0()) {
         setFromEndpoint(computedStartPt, arc1, 0);
     } else if (startAngle == arc1.theta2()) {
@@ -610,7 +606,7 @@ CircularArcIntersector::addArcIntersection(double startAngle, double endAngle, i
 }
 
 void
-CircularArcIntersector::addIntersection(const CoordinateXY& computedIntPt, const CircularArc& arc1, const CircularArc& arc2) {
+CircularArcIntersector::addArcArcIntersectionPoint(const CoordinateXY& computedIntPt, const CircularArc& arc1, const CircularArc& arc2) {
     CoordinateXYZM& newIntPt = intPt[nPt++];
     newIntPt = computedIntPt;
 
@@ -630,7 +626,9 @@ CircularArcIntersector::addIntersection(const CoordinateXY& computedIntPt, const
 }
 
 void
-CircularArcIntersector::addIntersection(const CoordinateXY& computedIntPt, const CircularArc& arc1, const CoordinateSequence& seq, std::size_t pos0, std::size_t pos1, bool useSegEndpoints) {
+CircularArcIntersector::addArcSegmentIntersectionPoint(const CoordinateXY& computedIntPt, const CircularArc& arc1,
+    const CoordinateSequence& seq, std::size_t pos0, std::size_t pos1, bool useSegEndpoints)
+{
     CoordinateXYZM& newIntPt = intPt[nPt++];
     newIntPt = computedIntPt;
 
diff --git a/tests/unit/algorithm/AngleTest.cpp b/tests/unit/algorithm/AngleTest.cpp
index f44d3ee42..17f83056f 100644
--- a/tests/unit/algorithm/AngleTest.cpp
+++ b/tests/unit/algorithm/AngleTest.cpp
@@ -232,6 +232,30 @@ void object::test<7>()
     }
 }
 
+template<>
+template<>
+void object::test<8>()
+{
+    set_test_name("Angle::nextCCW");
+
+    ensure_equals(Angle::nextCCW(Angle::PI_OVER_2, Angle::PI_OVER_4, PI), PI);
+    ensure_equals(Angle::nextCCW(Angle::PI_OVER_4, Angle::PI_OVER_2, PI), Angle::PI_OVER_2);
+    ensure_equals(Angle::nextCCW(Angle::PI_OVER_2, Angle::PI_OVER_4, Angle::PI_TIMES_2), Angle::PI_TIMES_2);
+    ensure_equals(Angle::nextCCW(Angle::PI_TIMES_2, Angle::PI_OVER_4, 0), 0);
+}
+
+template<>
+template<>
+void object::test<9>()
+{
+    set_test_name("Angle::fractionCCW");
+
+    ensure_equals("pi/2 in interval [2pi, pi]", Angle::fractionCCW(Angle::PI_OVER_2, Angle::PI_TIMES_2, PI), 0.5);
+    ensure_equals("3pi/2 in interval [pi, 0]", Angle::fractionCCW(3*Angle::PI_OVER_2, PI, 0), 0.5);
+    ensure_equals("3pi/2 in interval [pi, 2pi]", Angle::fractionCCW(3*Angle::PI_OVER_2, PI, Angle::PI_TIMES_2), 0.5);
+    ensure_equals("0 in interval [3pi/2, pi/2]", Angle::fractionCCW(0,  3*Angle::PI_OVER_2, Angle::PI_OVER_2), 0.5);
+    ensure_equals("2pi in interval [3pi/2, pi]", Angle::fractionCCW(Angle::PI_TIMES_2,  3*Angle::PI_OVER_2, PI), 1.0/3);
+}
 
 } // namespace tut
 

commit 8447ff2a32dd20703e0841736a8a3a6bd6be88f5
Author: Daniel Baston <dbaston at gmail.com>
Date:   Thu Dec 18 14:05:52 2025 -0500

    CircularArcIntersector: Remove disabled alternate implementation

diff --git a/src/algorithm/CircularArcIntersector.cpp b/src/algorithm/CircularArcIntersector.cpp
index 39b0ecead..ba0ab0ccd 100644
--- a/src/algorithm/CircularArcIntersector.cpp
+++ b/src/algorithm/CircularArcIntersector.cpp
@@ -369,7 +369,7 @@ CircularArcIntersector::intersects(const CircularArc& arc1, const CircularArc& a
     if (a == 0 || (d == 0 && r1 == r2)) {
         computeCocircularIntersection(arc1, arc2);
     } else {
-        // Add endpoint intersections that may be missed or inexactly computed.
+        // Explicitly add endpoint intersections that may be missed or inexactly computed.
         if (nPt < 2 && arc1.p0().equals2D(arc2.p0()) && (nPt == 0 || !intPt[0].equals2D(arc1.p0()))) {
             addIntersection(arc1.p0(), arc1, arc2);
         }
@@ -384,10 +384,10 @@ CircularArcIntersector::intersects(const CircularArc& arc1, const CircularArc& a
         }
 
         if (nPt < 2) {
+            // Compute interior intersection points.
             const double dx = c2.x-c1.x;
             const double dy = c2.y-c1.y;
 
-#if 1
             // point where a line between the two circle center points intersects
             // the radical line
             CoordinateXY p{c1.x + a* dx/d, c1.y+a* dy/d};
@@ -418,35 +418,6 @@ CircularArcIntersector::intersects(const CircularArc& arc1, const CircularArc& a
                     addIntersection(computedIntPt, arc1, arc2);
                 }
             }
-#else
-            // Alternate formulation.
-            // Instead of calculating the intersection points and determining if they fall on the arc,
-            // calculate the angles of the intersection points. If they fall on the arc, create intersection points
-            // at those angles.
-
-            double centerPointAngle = std::atan2(dy, dx);
-
-            double arc1IntPtAngleDeviation = std::acos(a / r1);
-
-            double a11 = Angle::normalize(centerPointAngle - arc1IntPtAngleDeviation);
-            double a12 = Angle::normalize(centerPointAngle + arc1IntPtAngleDeviation);
-
-            double b = d - a;
-            double arc2IntPtAngleDeviation = std::acos(b / r2);
-
-            double a21 = Angle::normalize(centerPointAngle + MATH_PI + arc2IntPtAngleDeviation);
-            double a22 = Angle::normalize(centerPointAngle + MATH_PI - arc2IntPtAngleDeviation);
-
-            if (arc1.containsAngle(a11) && arc2.containsAngle(a21)) {
-                intPt[nPt++] = CircularArcs::createPoint(arc1.getCenter(), arc1.getRadius(), a11);
-            }
-            if (arc1.containsAngle(a12) && arc2.containsAngle(a22)) {
-                intPt[nPt++] = CircularArcs::createPoint(arc1.getCenter(), arc1.getRadius(), a12);
-                if (nPt == 2 && intPt[0].equals(intPt[1])) {
-                    nPt = 1;
-                }
-            }
-#endif
         }
     }
 

commit 3efe43f99214c8e3fa3c0e76f3c991331f897c41
Author: Daniel Baston <dbaston at gmail.com>
Date:   Thu Dec 18 13:48:31 2025 -0500

    CircularArcIntersector: Prevent generation of too-close points

diff --git a/include/geos/algorithm/CircularArcIntersector.h b/include/geos/algorithm/CircularArcIntersector.h
index c0aa3b753..80e1543ab 100644
--- a/include/geos/algorithm/CircularArcIntersector.h
+++ b/include/geos/algorithm/CircularArcIntersector.h
@@ -138,6 +138,8 @@ private:
     static int
     circleIntersects(const CoordinateXY& center, double r, const CoordinateXY& p0, const CoordinateXY& p1, CoordinateXY& isect0, CoordinateXY& isect1);
 
+    void computeCocircularIntersection(const CircularArc& arc1, const CircularArc& arc2);
+
     std::array<CoordinateXYZM, 2> intPt;
     std::array<CircularArc, 2> intArc;
     intersection_type result = NO_INTERSECTION;
diff --git a/src/algorithm/CircularArcIntersector.cpp b/src/algorithm/CircularArcIntersector.cpp
index ae9e85758..39b0ecead 100644
--- a/src/algorithm/CircularArcIntersector.cpp
+++ b/src/algorithm/CircularArcIntersector.cpp
@@ -345,7 +345,7 @@ CircularArcIntersector::intersects(const CircularArc& arc1, const CircularArc& a
     const auto r1 = arc1.getRadius();
     const auto r2 = arc2.getRadius();
 
-    auto d = c1.distance(c2);
+    const auto d = c1.distance(c2);
 
     if (d > r1 + r2) {
         // Circles are disjoint
@@ -360,157 +360,16 @@ CircularArcIntersector::intersects(const CircularArc& arc1, const CircularArc& a
     }
 
     // a: the distance from c1 to the "radical line", which connects the two intersection points
-    // The following expression was rewritten by
-    double a = (d*d + r1*r1 - r2*r2) / (2*d);
     // Expression rewritten by Herbie, https://herbie.uwplse.org/demo/
-    // double a = std::fma(r1-r2, (r1 + r2) / (d+d), d*0.5);
+    // const double a = (d*d + r1*r1 - r2*r2) / (2*d);
+    const double a = std::fma(r1-r2, (r1 + r2) / (d+d), d*0.5);
 
     // TODO because the circle center calculation is inexact we need some kind of tolerance here.
     // Take a PrecisionModel like LineIntersector?
     if (a == 0 || (d == 0 && r1 == r2)) {
-        // COCIRCULAR
-
-        double ap0 = arc1.theta0();
-        double ap1 = arc1.theta2();
-        double bp0 = arc2.theta0();
-        double bp1 = arc2.theta2();
-
-        // Orientation of the result matches the first input
-        bool resultArcIsCCW = true;
-
-        // Make both inputs counter-clockwise for the purpose of determining intersections
-        if (arc1.getOrientation() != Orientation::COUNTERCLOCKWISE) {
-            std::swap(ap0, ap1);
-            resultArcIsCCW = false;
-        }
-        if (arc2.getOrientation() != Orientation::COUNTERCLOCKWISE) {
-            std::swap(bp0, bp1);
-        }
-        ap0 = Angle::normalizePositive(ap0);
-        ap1 = Angle::normalizePositive(ap1);
-        bp0 = Angle::normalizePositive(bp0);
-        bp1 = Angle::normalizePositive(bp1);
-
-        bool checkBp1inA = true;
-        bool checkAcontained = true;
-
-        // Possible intersection arrangements:
-        // A contained within B
-        // A overlaps B
-        // B contained within A
-
-        // check start of B within A?
-        if (Angle::isWithinCCW(bp0, ap0, ap1)) {
-            checkAcontained = false;
-            const double start = bp0;
-            const double end = nextAngleCCW(start, bp1, ap1);
-
-            if (end == bp1) {
-                checkBp1inA = false;
-            }
-
-            if (start == end) {
-                const CoordinateXY computedIntPt = CircularArcs::createPoint(c1, r1, start);
-                addIntersection(computedIntPt, arc1, arc2);
-            }
-            else {
-                if (resultArcIsCCW) {
-                    addArcIntersection(start, end, Orientation::COUNTERCLOCKWISE, arc1, arc2);
-                }
-                else {
-                    addArcIntersection(end, start, Orientation::CLOCKWISE, arc1, arc2);
-                }
-            }
-        }
-
-        if (checkBp1inA && Angle::isWithinCCW(bp1, ap0, ap1)) {
-            // end of B within A?
-            checkAcontained = false;
-
-            const double start = ap0;
-            const double end = bp1;
-            if (start == end) {
-                const CoordinateXY computedIntPt = CircularArcs::createPoint(c1, r1, start);
-                addIntersection(computedIntPt, arc1, arc2);
-            }
-            else {
-                if (resultArcIsCCW) {
-                    addArcIntersection(start, end, Orientation::CLOCKWISE, arc1, arc2);
-                }
-                else {
-                    addArcIntersection(end, start, Orientation::CLOCKWISE, arc1, arc2);
-                }
-            }
-        }
-
-        if (checkAcontained && Angle::isWithinCCW(ap0, bp0 , bp1) && ap0 != bp0  && ap0 != bp1 && Angle::isWithinCCW(ap1, bp0, bp1) && ap1 != bp1 && ap1 != bp0) {
-            if (resultArcIsCCW) {
-                addArcIntersection(ap0, ap1, Orientation::COUNTERCLOCKWISE, arc1, arc2);
-            }
-            else {
-                addArcIntersection(ap1, ap0, Orientation::CLOCKWISE, arc1, arc2);
-            }
-        }
+        computeCocircularIntersection(arc1, arc2);
     } else {
-        // NOT COCIRCULAR
-
-        const double dx = c2.x-c1.x;
-        const double dy = c2.y-c1.y;
-
-#if 1
-        // point where a line between the two circle center points intersects
-        // the radical line
-        CoordinateXY p{c1.x + a* dx/d, c1.y+a* dy/d};
-
-        // distance from p to the intersection points
-        const double h = std::sqrt(r1*r1 - a*a);
-
-        const CoordinateXY isect0{p.x + h* dy/d, p.y - h* dx/d };
-        const CoordinateXY isect1{p.x - h* dy/d, p.y + h* dx/d };
-
-        for (const CoordinateXY& computedIntPt : {isect0, isect1}) {
-            if (nPt > 0 && computedIntPt.equals2D(intPt[0])) {
-                continue;
-            }
-
-            if (arc1.containsPointOnCircle(computedIntPt) && arc2.containsPointOnCircle(computedIntPt)) {
-                addIntersection(computedIntPt, arc1, arc2);
-            }
-        }
-#else
-        // Alternate formulation.
-        // Instead of calculating the intersection points and determining if they fall on the arc,
-        // calculate the angles of the intersection points. If they fall on the arc, create intersection points
-        // at those angles.
-
-        double centerPointAngle = std::atan2(dy, dx);
-
-        double arc1IntPtAngleDeviation = std::acos(a / r1);
-
-        double a11 = Angle::normalize(centerPointAngle - arc1IntPtAngleDeviation);
-        double a12 = Angle::normalize(centerPointAngle + arc1IntPtAngleDeviation);
-
-        double b = d - a;
-        double arc2IntPtAngleDeviation = std::acos(b / r2);
-
-        double a21 = Angle::normalize(centerPointAngle + MATH_PI + arc2IntPtAngleDeviation);
-        double a22 = Angle::normalize(centerPointAngle + MATH_PI - arc2IntPtAngleDeviation);
-
-        if (arc1.containsAngle(a11) && arc2.containsAngle(a21)) {
-            intPt[nPt++] = CircularArcs::createPoint(arc1.getCenter(), arc1.getRadius(), a11);
-        }
-        if (arc1.containsAngle(a12) && arc2.containsAngle(a22)) {
-            intPt[nPt++] = CircularArcs::createPoint(arc1.getCenter(), arc1.getRadius(), a12);
-            if (nPt == 2 && intPt[0].equals(intPt[1])) {
-                nPt = 1;
-            }
-        }
-#endif
-
-        // Add endpoint intersections missed due to precision issues.
-        // TODO: Add some logic to prevent double-counting of endpoints. Ideally, the endpoint test would happen before
-        // computing intersection points, so if there is an endpoint intersection we get the exact intersection point
-        // instead of a computed one.
+        // Add endpoint intersections that may be missed or inexactly computed.
         if (nPt < 2 && arc1.p0().equals2D(arc2.p0()) && (nPt == 0 || !intPt[0].equals2D(arc1.p0()))) {
             addIntersection(arc1.p0(), arc1, arc2);
         }
@@ -523,6 +382,72 @@ CircularArcIntersector::intersects(const CircularArc& arc1, const CircularArc& a
         if (nPt < 2 && arc1.p2().equals2D(arc2.p2()) && (nPt == 0 || !intPt[0].equals2D(arc1.p2()))) {
             addIntersection(arc1.p2(), arc1, arc2);
         }
+
+        if (nPt < 2) {
+            const double dx = c2.x-c1.x;
+            const double dy = c2.y-c1.y;
+
+#if 1
+            // point where a line between the two circle center points intersects
+            // the radical line
+            CoordinateXY p{c1.x + a* dx/d, c1.y+a* dy/d};
+
+            // distance from p to the intersection points
+            const double h = std::sqrt(r1*r1 - a*a);
+
+            CoordinateXY isect0{p.x + h* dy/d, p.y - h* dx/d };
+            CoordinateXY isect1{p.x - h* dy/d, p.y + h* dx/d };
+
+            // One of the computed intersection points may be an inexact version of an endpoint.
+            // If we already have an endpoint intersection, process the farther-away computed
+            // point first.
+            if (nPt == 1 && intPt[0].distance(isect0) < intPt[0].distance(isect1)) {
+                std::swap(isect0, isect1);
+            }
+
+            for (const CoordinateXY& computedIntPt : {isect0, isect1}) {
+                if (nPt > 0 && computedIntPt.equals2D(intPt[0])) {
+                    continue;
+                }
+
+                if (nPt > 1) {
+                    continue;
+                }
+
+                if (arc1.containsPointOnCircle(computedIntPt) && arc2.containsPointOnCircle(computedIntPt)) {
+                    addIntersection(computedIntPt, arc1, arc2);
+                }
+            }
+#else
+            // Alternate formulation.
+            // Instead of calculating the intersection points and determining if they fall on the arc,
+            // calculate the angles of the intersection points. If they fall on the arc, create intersection points
+            // at those angles.
+
+            double centerPointAngle = std::atan2(dy, dx);
+
+            double arc1IntPtAngleDeviation = std::acos(a / r1);
+
+            double a11 = Angle::normalize(centerPointAngle - arc1IntPtAngleDeviation);
+            double a12 = Angle::normalize(centerPointAngle + arc1IntPtAngleDeviation);
+
+            double b = d - a;
+            double arc2IntPtAngleDeviation = std::acos(b / r2);
+
+            double a21 = Angle::normalize(centerPointAngle + MATH_PI + arc2IntPtAngleDeviation);
+            double a22 = Angle::normalize(centerPointAngle + MATH_PI - arc2IntPtAngleDeviation);
+
+            if (arc1.containsAngle(a11) && arc2.containsAngle(a21)) {
+                intPt[nPt++] = CircularArcs::createPoint(arc1.getCenter(), arc1.getRadius(), a11);
+            }
+            if (arc1.containsAngle(a12) && arc2.containsAngle(a22)) {
+                intPt[nPt++] = CircularArcs::createPoint(arc1.getCenter(), arc1.getRadius(), a12);
+                if (nPt == 2 && intPt[0].equals(intPt[1])) {
+                    nPt = 1;
+                }
+            }
+#endif
+        }
     }
 
     if (nArc) {
@@ -575,6 +500,95 @@ setFromEndpoint(geom::CoordinateXYZM& pt, const CircularArc& arc, std::size_t in
     });
 }
 
+void
+CircularArcIntersector::computeCocircularIntersection(const CircularArc& arc1, const CircularArc& arc2)
+{
+    const auto& center = arc1.getCenter();
+    const double radius = arc1.getRadius();
+
+    double ap0 = arc1.theta0();
+    double ap1 = arc1.theta2();
+    double bp0 = arc2.theta0();
+    double bp1 = arc2.theta2();
+
+    // Orientation of the result matches the first input
+        bool resultArcIsCCW = true;
+
+        // Make both inputs counter-clockwise for the purpose of determining intersections
+        if (arc1.getOrientation() != Orientation::COUNTERCLOCKWISE) {
+            std::swap(ap0, ap1);
+            resultArcIsCCW = false;
+        }
+        if (arc2.getOrientation() != Orientation::COUNTERCLOCKWISE) {
+            std::swap(bp0, bp1);
+        }
+        ap0 = Angle::normalizePositive(ap0);
+        ap1 = Angle::normalizePositive(ap1);
+        bp0 = Angle::normalizePositive(bp0);
+        bp1 = Angle::normalizePositive(bp1);
+
+        bool checkBp1inA = true;
+        bool checkAcontained = true;
+
+        // Possible intersection arrangements:
+        // A contained within B
+        // A overlaps B
+        // B contained within A
+
+        // check start of B within A?
+        if (Angle::isWithinCCW(bp0, ap0, ap1)) {
+            checkAcontained = false;
+            const double start = bp0;
+            const double end = nextAngleCCW(start, bp1, ap1);
+
+            if (end == bp1) {
+                checkBp1inA = false;
+            }
+
+            if (start == end) {
+                const CoordinateXY computedIntPt = CircularArcs::createPoint(center, radius, start);
+                addIntersection(computedIntPt, arc1, arc2);
+            }
+            else {
+                if (resultArcIsCCW) {
+                    addArcIntersection(start, end, Orientation::COUNTERCLOCKWISE, arc1, arc2);
+                }
+                else {
+                    addArcIntersection(end, start, Orientation::CLOCKWISE, arc1, arc2);
+                }
+            }
+        }
+
+        if (checkBp1inA && Angle::isWithinCCW(bp1, ap0, ap1)) {
+            // end of B within A?
+            checkAcontained = false;
+
+            const double start = ap0;
+            const double end = bp1;
+            if (start == end) {
+                const CoordinateXY computedIntPt = CircularArcs::createPoint(center, radius, start);
+                addIntersection(computedIntPt, arc1, arc2);
+            }
+            else {
+                if (resultArcIsCCW) {
+                    addArcIntersection(start, end, Orientation::CLOCKWISE, arc1, arc2);
+                }
+                else {
+                    addArcIntersection(end, start, Orientation::CLOCKWISE, arc1, arc2);
+                }
+            }
+        }
+
+        if (checkAcontained && Angle::isWithinCCW(ap0, bp0 , bp1) && ap0 != bp0  && ap0 != bp1 && Angle::isWithinCCW(ap1, bp0, bp1) && ap1 != bp1 && ap1 != bp0) {
+            if (resultArcIsCCW) {
+                addArcIntersection(ap0, ap1, Orientation::COUNTERCLOCKWISE, arc1, arc2);
+            }
+            else {
+                addArcIntersection(ap1, ap0, Orientation::CLOCKWISE, arc1, arc2);
+            }
+        }
+}
+
 void
 CircularArcIntersector::addArcIntersection(double startAngle, double endAngle, int orientation, const CircularArc& arc1, const CircularArc& arc2)
 {
diff --git a/tests/unit/capi/GEOSNodeTest.cpp b/tests/unit/capi/GEOSNodeTest.cpp
index 23b3c2172..721a8c46a 100644
--- a/tests/unit/capi/GEOSNodeTest.cpp
+++ b/tests/unit/capi/GEOSNodeTest.cpp
@@ -231,8 +231,12 @@ void object::test<10>()
     ensure(result_ != nullptr);
 
     expected_ = fromWKT("MULTICURVE ZM ("
-                        "CIRCULARSTRING ZM (-1 0 3 4, -1 1.2246467991e-16 5 4.75, -1 2.7755575616e-16 7 5.5, -1 1.2246467991e-16 7 5.5, -1 5.5511151231e-17 7 5.5, -0.7071067812 0.7071067812 5.25 7.375, -2.7755575616e-16 1 3.5 9.25, -3.8285686989e-16 1 3.5 9.25, -5.5511151231e-17 1 3.5 9.25, 0.7071067812 0.7071067812 3.75 8.125, 1 0 4 7),"
-                        "CIRCULARSTRING ZM (-1 2 NaN 9, -0.2928932188 1.7071067812 NaN 9.125, -2.7755575616e-16 1 3.5 9.25, 0 1 3.5 9.25, -5.5511151231e-17 1 3.5 9.25, -0.2928932188 0.2928932188 5.25 7.375, -1 2.7755575616e-16 7 5.5, -1 0 7 5.5, -1 5.5511151231e-17 7 5.5, -1 0 NaN 11.25, -1 0 NaN 17))");
+        "CIRCULARSTRING ZM (-1 0 3 4, -0.7071067812 0.7071067812 2.5 6.5, -5.5511151231e-17 1 2 9, 0.7071067812 0.7071067812 3 8, 1 0 4 7),"
+        "CIRCULARSTRING ZM (-1 2 NaN 9, -0.2928932188 1.7071067812 NaN 9, -5.5511151231e-17 1 2 9, -0.2928932188 0.2928932188 2.5 6.5, -1 0 3 4))");
+
+    ensure_equals(GEOSGetNumGeometries(result_), 2);
+    ensure_equals("Noded arc 1 should have 5 points", GEOSGeomGetNumPoints(GEOSGetGeometryN(result_, 0)), 5);
+    ensure_equals("Noded arc 2 should have 5 points", GEOSGeomGetNumPoints(GEOSGetGeometryN(result_, 1)), 5);
 
     ensure_equals_exact_geometry_xyzm(reinterpret_cast<Geometry*>(result_),
                                       reinterpret_cast<Geometry*>(expected_), 1e-8);
@@ -251,7 +255,7 @@ void object::test<11>()
     ensure(result_ != nullptr);
 
     expected_ = fromWKT("MULTICURVE ZM ("
-                        "CIRCULARSTRING ZM (-5 0 3 4, -3.5355 3.5355 3 6, 0 5 3 8, 3.5355 3.5355 3.5 7.5, 4 3 4 7),"
+                        "CIRCULARSTRING ZM (-5 0 3 4, -3.5355 3.5355 3 6, 0 5 3 8, 2.2361 4.4721 3.5 7.5, 4 3 4 7),"
                         "LINESTRING ZM (0 0 NaN 7, 0 5 3 8),"
                         "LINESTRING ZM (0 5 3 8, 0 10 NaN 13))");
 
@@ -303,7 +307,7 @@ void object::test<14>()
 {
     set_test_name("CIRCULARSTRING Z / LINESTRING Z endpoint intersection");
 
-    input_ = fromWKT("MULTICURVE (CIRCULARSTRING Z (-5 0 3, -4 3 5, 4 3 6), LINESTRING Z (0 0 7, 4 3 13))");
+    input_ = fromWKT("MULTICURVE (CIRCULARSTRING Z (-4 3 3, 0 5 5, 4 3 7), LINESTRING Z (0 0 7, 4 3 13))");
     ensure(input_);
 
     result_ = GEOSNode(input_);

commit 114887e4058d97ead9ccc22269300ad51aa51e56
Author: Daniel Baston <dbaston at gmail.com>
Date:   Thu Dec 18 13:39:09 2025 -0500

    geos_unit: prevent asserts from skipping unhandled geometry types

diff --git a/tests/unit/utility.h b/tests/unit/utility.h
index 6b98a59d0..0a15532ed 100644
--- a/tests/unit/utility.h
+++ b/tests/unit/utility.h
@@ -388,7 +388,7 @@ ensure_equals_exact_geometry_xyz(const geos::geom::Geometry *lhs_in,
     assert(nullptr != rhs_in);
 
     using geos::geom::Point;
-    using geos::geom::LineString;
+    using geos::geom::SimpleCurve;
     using geos::geom::Polygon;
     using geos::geom::CoordinateSequence;
     using geos::geom::GeometryCollection;
@@ -401,19 +401,19 @@ ensure_equals_exact_geometry_xyz(const geos::geom::Geometry *lhs_in,
       const Point *gpt2 = static_cast<const Point *>(rhs_in);
       return ensure_equals_dims( gpt1->getCoordinatesRO(), gpt2->getCoordinatesRO(), 3, tolerance);
     }
-    else if (const LineString* gln1 = dynamic_cast<const LineString *>(lhs_in)) {
-      const LineString *gln2 = static_cast<const LineString *>(rhs_in);
+    else if (const SimpleCurve* gln1 = dynamic_cast<const SimpleCurve *>(lhs_in)) {
+      const SimpleCurve *gln2 = static_cast<const SimpleCurve *>(rhs_in);
       return ensure_equals_dims( gln1->getCoordinatesRO(), gln2->getCoordinatesRO(), 3, tolerance);
     }
-    else if (dynamic_cast<const Polygon *>(lhs_in)) {
-      ensure("Not implemented yet", 0);
-    }
     else if (const GeometryCollection* gc1 = dynamic_cast<const GeometryCollection *>(lhs_in)) {
       const GeometryCollection *gc2 = static_cast<const GeometryCollection *>(rhs_in);
       for (unsigned int i = 0; i < gc1->getNumGeometries(); i++) {
         ensure_equals_exact_geometry_xyz(gc1->getGeometryN(i), gc2->getGeometryN(i), tolerance);
       }
     }
+    else {
+        fail("Not implemented yet");
+    }
 }
 
 inline void
@@ -445,8 +445,9 @@ ensure_equals_exact_geometry_xyzm(const geos::geom::Geometry *lhs_in,
     assert(nullptr != rhs_in);
 
     using geos::geom::Point;
-    using geos::geom::LineString;
-    using geos::geom::Polygon;
+    using geos::geom::Curve;
+    using geos::geom::SimpleCurve;
+    using geos::geom::Surface;
     using geos::geom::CoordinateSequence;
     using geos::geom::GeometryCollection;
 
@@ -457,14 +458,14 @@ ensure_equals_exact_geometry_xyzm(const geos::geom::Geometry *lhs_in,
       const Point *gpt2 = static_cast<const Point *>(rhs_in);
       return ensure_equals_exact_xyzm(gpt1->getCoordinatesRO(), gpt2->getCoordinatesRO(), tolerance);
     }
-    else if (const LineString* gln1 = dynamic_cast<const LineString *>(lhs_in)) {
-      const LineString *gln2 = static_cast<const LineString *>(rhs_in);
+    else if (const SimpleCurve* gln1 = dynamic_cast<const SimpleCurve*>(lhs_in)) {
+      const SimpleCurve *gln2 = static_cast<const SimpleCurve*>(rhs_in);
       return ensure_equals_exact_xyzm(gln1->getCoordinatesRO(), gln2->getCoordinatesRO(), tolerance);
     }
-    else if (const Polygon* gply1 = dynamic_cast<const Polygon*>(lhs_in)) {
-      const Polygon* gply2 = static_cast<const Polygon*>(rhs_in);
-      const LinearRing* extRing1 = gply1->getExteriorRing();
-      const LinearRing* extRing2 = gply2->getExteriorRing();
+    else if (const Surface* gply1 = dynamic_cast<const Surface*>(lhs_in)) {
+      const Surface* gply2 = static_cast<const Surface*>(rhs_in);
+      const Curve* extRing1 = gply1->getExteriorRing();
+      const Curve* extRing2 = gply2->getExteriorRing();
 
       ensure_equals_exact_geometry_xyzm(extRing1, extRing2, tolerance);
 
@@ -483,6 +484,8 @@ ensure_equals_exact_geometry_xyzm(const geos::geom::Geometry *lhs_in,
       for (unsigned int i = 0; i < gc1->getNumGeometries(); i++) {
         ensure_equals_exact_geometry_xyzm(gc1->getGeometryN(i), gc2->getGeometryN(i), tolerance);
       }
+    } else {
+        fail("Not implemented yet.");
     }
 }
 
@@ -548,14 +551,14 @@ ensure_equals_exact_geometry(const geos::geom::Geometry *lhs_in,
       const LineString *gln2 = static_cast<const LineString *>(rhs_in);
       return ensure_equals_dims( gln1->getCoordinatesRO(), gln2->getCoordinatesRO(), 2, tolerance);
     }
-    else if (dynamic_cast<const Polygon *>(lhs_in)) {
-      ensure("Not implemented yet", 0);
-    }
     else if (const GeometryCollection* gc1 = dynamic_cast<const GeometryCollection *>(lhs_in)) {
-      const GeometryCollection *gc2 = static_cast<const GeometryCollection *>(rhs_in);
-      for (unsigned int i = 0; i < gc1->getNumGeometries(); i++) {
-        ensure_equals_exact_geometry(gc1->getGeometryN(i), gc2->getGeometryN(i), tolerance);
-      }
+        const GeometryCollection *gc2 = static_cast<const GeometryCollection *>(rhs_in);
+        for (unsigned int i = 0; i < gc1->getNumGeometries(); i++) {
+            ensure_equals_exact_geometry(gc1->getGeometryN(i), gc2->getGeometryN(i), tolerance);
+        }
+    }
+    else {
+        fail("Not implemented yet");
     }
 }
 

commit 7d701f5e5077fe860298bd29802922a206b6d70e
Author: Daniel Baston <dbaston at gmail.com>
Date:   Mon Dec 15 13:50:17 2025 -0500

    ArcString: Adopt shared_ptr<const CoordinateSequence> representation

diff --git a/include/geos/noding/ArcString.h b/include/geos/noding/ArcString.h
index c442a26f6..67285f33c 100644
--- a/include/geos/noding/ArcString.h
+++ b/include/geos/noding/ArcString.h
@@ -62,11 +62,11 @@ public:
         return m_arcs.end();
     }
 
-    std::unique_ptr<geom::CoordinateSequence> releaseCoordinates();
+    const std::shared_ptr<const geom::CoordinateSequence>& getCoordinates() const override;
 
 protected:
     std::vector<geom::CircularArc> m_arcs;
-    std::unique_ptr<geom::CoordinateSequence> m_seq;
+    std::shared_ptr<const geom::CoordinateSequence> m_seq;
     void* m_context;
 };
 
diff --git a/include/geos/noding/PathString.h b/include/geos/noding/PathString.h
index bc1a66990..0de86c23a 100644
--- a/include/geos/noding/PathString.h
+++ b/include/geos/noding/PathString.h
@@ -20,11 +20,14 @@
 #include <memory>
 #include <vector>
 
+namespace geos::geom {
+    class CoordinateSequence;
+}
+
 namespace geos::noding {
 
-/// A PathString represents a contiguous line/arc to the used as an input for
-/// noding. To access the coordinates, it is necessary to know whether they
-/// represent a set of line segments (SegmentString) or circular arcs (ArcString).
+/// A PathString represents a contiguous line/arc to be used as an input or output
+/// of a noding process.
 class GEOS_DLL PathString {
 public:
     virtual ~PathString() = default;
@@ -33,6 +36,11 @@ public:
 
     virtual double getLength() const = 0;
 
+    /// \brief
+    /// Return a pointer to the CoordinateSequence associated
+    /// with this PathString.
+    virtual const std::shared_ptr<const geom::CoordinateSequence>& getCoordinates() const = 0;
+
     std::vector<PathString*>
     static toRawPointerVector(const std::vector<std::unique_ptr<PathString>> & segStrings);
 };
diff --git a/include/geos/noding/SegmentString.h b/include/geos/noding/SegmentString.h
index 6648386df..d13c70fda 100644
--- a/include/geos/noding/SegmentString.h
+++ b/include/geos/noding/SegmentString.h
@@ -111,7 +111,7 @@ public:
     /// \brief
     /// Return a pointer to the CoordinateSequence associated
     /// with this SegmentString.
-    const std::shared_ptr<const geom::CoordinateSequence>& getCoordinates() const {
+    const std::shared_ptr<const geom::CoordinateSequence>& getCoordinates() const override {
         return seq;
     }
 
diff --git a/src/noding/ArcString.cpp b/src/noding/ArcString.cpp
index 6095414c2..e0b0ce7df 100644
--- a/src/noding/ArcString.cpp
+++ b/src/noding/ArcString.cpp
@@ -15,8 +15,9 @@
 #include <geos/noding/ArcString.h>
 
 namespace geos::noding {
- std::unique_ptr<geom::CoordinateSequence>
- ArcString::releaseCoordinates() {
-  return std::move(m_seq);
- }
+const std::shared_ptr<const geom::CoordinateSequence>&
+ArcString::getCoordinates() const
+{
+    return m_seq;
+}
 }
\ No newline at end of file
diff --git a/src/noding/GeometryNoder.cpp b/src/noding/GeometryNoder.cpp
index 0aaeeb89e..d65676f76 100644
--- a/src/noding/GeometryNoder.cpp
+++ b/src/noding/GeometryNoder.cpp
@@ -136,7 +136,7 @@ GeometryNoder::toGeometry(std::vector<std::unique_ptr<PathString>>& nodedEdges)
             resultArcs = true;
             auto* as = dynamic_cast<ArcString*>(path.get());
             // FIXME: check for duplicates
-            lines.push_back(geomFact->createCircularString(as->releaseCoordinates()));
+            lines.push_back(geomFact->createCircularString(as->getCoordinates()));
         }
     }
 

commit 0de2f8fb56f74890702f7abd7532833b4e48f606
Author: Daniel Baston <dbaston at gmail.com>
Date:   Mon Dec 15 13:03:54 2025 -0500

    GEOSNode: Add tests for endpoint intersections

diff --git a/src/noding/ArcIntersectionAdder.cpp b/src/noding/ArcIntersectionAdder.cpp
index b85cbb673..61b9adae1 100644
--- a/src/noding/ArcIntersectionAdder.cpp
+++ b/src/noding/ArcIntersectionAdder.cpp
@@ -54,8 +54,7 @@ ArcIntersectionAdder::processIntersections(ArcString& e0, std::size_t segIndex0,
 void
 ArcIntersectionAdder::processIntersections(ArcString& e0, std::size_t segIndex0, SegmentString& e1, std::size_t segIndex1)
 {
-// don't bother intersecting a segment with itself
-
+    // don't bother intersecting a segment with itself
     const geom::CircularArc& arc = e0.getArc(segIndex0);
 
     // FIXME get useSegEndpoints from somewhere
@@ -79,7 +78,7 @@ ArcIntersectionAdder::processIntersections(SegmentString& e0, std::size_t segInd
 {
     using geom::CoordinateXY;
 
-// don't bother intersecting a segment with itself
+    // don't bother intersecting a segment with itself
     if(&e0 == &e1 && segIndex0 == segIndex1) {
         return;
     }
@@ -87,20 +86,11 @@ ArcIntersectionAdder::processIntersections(SegmentString& e0, std::size_t segInd
     m_intersector.intersects(*e0.getCoordinates(), segIndex0, segIndex0 + 1,
                              *e1.getCoordinates(), segIndex1, segIndex1 + 1);
 
-#if 0
-    const CoordinateXY& p0 = e0.getCoordinate(segIndex0);
-    const CoordinateXY& p1 = e0.getCoordinate(segIndex0 + 1);
-    const CoordinateXY& q0 = e1.getCoordinate(segIndex1);
-    const CoordinateXY& q1 = e1.getCoordinate(segIndex1 + 1);
-
-    m_intersector.intersects(p0, p1, q0, q1);
-#endif
-
     if (m_intersector.getResult() == algorithm::CircularArcIntersector::NO_INTERSECTION) {
         return;
     }
 
-// todo collinear?
+    // todo collinear?
 
     static_cast<NodedSegmentString&>(e0).addIntersection(m_intersector.getPoint(0), segIndex0);
 
diff --git a/tests/unit/capi/GEOSNodeTest.cpp b/tests/unit/capi/GEOSNodeTest.cpp
index f2f3d2607..23b3c2172 100644
--- a/tests/unit/capi/GEOSNodeTest.cpp
+++ b/tests/unit/capi/GEOSNodeTest.cpp
@@ -242,7 +242,7 @@ template<>
 template<>
 void object::test<11>()
 {
-    set_test_name("CIRCULARSTRING ZM intersecting LINESTRING M");
+    set_test_name("CIRCULARSTRING ZM / LINESTRING M interior intersection");
 
     input_ = fromWKT("MULTICURVE (CIRCULARSTRING ZM (-5 0 3 4, -4 3 2 5, 4 3 4 7), LINESTRING M (0 0 7, 0 10 13))");
     ensure(input_);
@@ -279,5 +279,77 @@ void object::test<12>()
                                       reinterpret_cast<Geometry*>(expected_), 1e-4);
 }
 
+template<>
+template<>
+void object::test<13>()
+{
+    set_test_name("LINESTRING Z / LINESTRING Z endpoint intersection");
+
+    input_ = fromWKT("MULTILINESTRING Z ((-5 0 3, 4 3 6), (0 0 7, 4 3 13))");
+    ensure(input_);
+
+    result_ = GEOSNode(input_);
+    ensure(result_ != nullptr);
+
+    expected_ = GEOSGeom_clone(input_);
+
+    ensure_equals_exact_geometry_xyzm(reinterpret_cast<Geometry*>(result_),
+                                      reinterpret_cast<Geometry*>(expected_), 1e-4);
+}
+
+template<>
+template<>
+void object::test<14>()
+{
+    set_test_name("CIRCULARSTRING Z / LINESTRING Z endpoint intersection");
+
+    input_ = fromWKT("MULTICURVE (CIRCULARSTRING Z (-5 0 3, -4 3 5, 4 3 6), LINESTRING Z (0 0 7, 4 3 13))");
+    ensure(input_);
+
+    result_ = GEOSNode(input_);
+    ensure(result_ != nullptr);
+
+    expected_ = GEOSGeom_clone(input_);
+
+    ensure_equals_exact_geometry_xyzm(reinterpret_cast<Geometry*>(result_),
+                                      reinterpret_cast<Geometry*>(expected_), 1e-4);
+}
+
+template<>
+template<>
+void object::test<15>()
+{
+    set_test_name("LINESTRING Z / LINESTRING endpoint intersection");
+
+    input_ = fromWKT("GEOMETRYCOLLECTION (LINESTRING Z (-5 0 3, 4 3 6), LINESTRING (0 0, 4 3))");
+    ensure(input_);
+
+    result_ = GEOSNode(input_);
+    ensure(result_ != nullptr);
+
+    expected_ = fromWKT("MULTILINESTRING Z ((-5 0 3, 4 3 6), (0 0 NaN, 4 3 NaN))");
+
+    ensure_equals_exact_geometry_xyzm(reinterpret_cast<Geometry*>(result_),
+                                      reinterpret_cast<Geometry*>(expected_), 1e-4);
+}
+
+template<>
+template<>
+void object::test<16>()
+{
+    set_test_name("CIRCULARSTRING Z / LINESTRING endpoint intersection");
+
+    input_ = fromWKT("MULTICURVE (CIRCULARSTRING Z (-5 0 3, -4 3 5, 4 3 6), LINESTRING (0 0, 4 3))");
+    ensure(input_);
+
+    result_ = GEOSNode(input_);
+    ensure(result_ != nullptr);
+
+    expected_ = fromWKT("MULTICURVE Z (CIRCULARSTRING Z (-5 0 3, -1.5811388301 4.7434164903 4.5, 4 3 6), (0 0 NaN, 4 3 NaN))");
+
+    ensure_equals_exact_geometry_xyzm(reinterpret_cast<Geometry*>(result_),
+                                      reinterpret_cast<Geometry*>(expected_), 1e-4);
+}
+
 } // namespace tut
 

commit c181764a77beae6e05529eff91811c6a7c53f4ce
Author: Daniel Baston <dbaston at gmail.com>
Date:   Mon Dec 15 12:42:27 2025 -0500

    ArcIntersectionAdder: Handle cocircular intersection

diff --git a/include/geos/noding/NodableArcString.h b/include/geos/noding/NodableArcString.h
index 207f497b3..2fcd5be2d 100644
--- a/include/geos/noding/NodableArcString.h
+++ b/include/geos/noding/NodableArcString.h
@@ -23,13 +23,13 @@ namespace geos::noding {
 class GEOS_DLL NodableArcString : public ArcString, public NodablePath {
 
 public:
-    //using ArcString::ArcString;
-
     NodableArcString(std::vector<geom::CircularArc> arcs, std::unique_ptr<geom::CoordinateSequence> coords, bool constructZ, bool constructM, void* context);
 
     std::unique_ptr<ArcString> clone() const;
 
-    void addIntersection(geom::CoordinateXYZM intPt, size_t segmentIndex) override {
+    using NodablePath::addIntersection;
+
+    void addIntersection(const geom::CoordinateXYZM& intPt, size_t segmentIndex) override {
         m_adds[segmentIndex].push_back(intPt);
     }
 
diff --git a/include/geos/noding/NodablePath.h b/include/geos/noding/NodablePath.h
index 35a4234c4..58609d637 100644
--- a/include/geos/noding/NodablePath.h
+++ b/include/geos/noding/NodablePath.h
@@ -21,14 +21,20 @@ namespace geos::noding {
 
 /// A NodablePath represents a PathString to which coordinates can be added.
 class GEOS_DLL NodablePath {
-    //virtual void addIntersection( const geom::Coordinate& intPt, int segmentIndex) =0;
-    //virtual void addIntersection( const geom::CoordinateXYM& intPt, int segmentIndex) =0;
 public:
-    virtual void addInt(const geom::CoordinateXY& intPt, size_t pathIndex) {
+    virtual void addIntersection( const geom::Coordinate& intPt, size_t pathIndex) {
         addIntersection(geom::CoordinateXYZM{intPt}, pathIndex);
     }
 
-    virtual void addIntersection(geom::CoordinateXYZM intPt, size_t pathIndex) =0;
+    virtual void addIntersection( const geom::CoordinateXYM& intPt, size_t pathIndex) {
+        addIntersection(geom::CoordinateXYZM{intPt}, pathIndex);
+    }
+
+    virtual void addIntersection(const geom::CoordinateXY& intPt, size_t pathIndex) {
+        addIntersection(geom::CoordinateXYZM{intPt}, pathIndex);
+    }
+
+    virtual void addIntersection(const geom::CoordinateXYZM& intPt, size_t pathIndex) =0;
 };
 
 }
\ No newline at end of file
diff --git a/src/noding/ArcIntersectionAdder.cpp b/src/noding/ArcIntersectionAdder.cpp
index b6fb218c6..b85cbb673 100644
--- a/src/noding/ArcIntersectionAdder.cpp
+++ b/src/noding/ArcIntersectionAdder.cpp
@@ -12,7 +12,6 @@
  *
  **********************************************************************/
 
-#include <geos/geom/CoordinateSequences.h>
 #include <geos/noding/ArcIntersectionAdder.h>
 #include <geos/noding/NodableArcString.h>
 #include <geos/noding/NodedSegmentString.h>
@@ -36,11 +35,20 @@ ArcIntersectionAdder::processIntersections(ArcString& e0, std::size_t segIndex0,
         return;
     }
 
-    // TODO handle cocircular intersections
     for (std::uint8_t i = 0; i < m_intersector.getNumPoints(); i++) {
         detail::down_cast<NodableArcString*>(&e0)->addIntersection(m_intersector.getPoint(i), segIndex0);
         detail::down_cast<NodableArcString*>(&e1)->addIntersection(m_intersector.getPoint(i), segIndex1);
     }
+
+    for (std::uint8_t i = 0; i < m_intersector.getNumArcs(); i++) {
+        const auto& arc = m_intersector.getArc(i);
+        for (size_t j : {0u, 2u}) {
+            arc.applyAt(j, [&e0, &segIndex0, &e1, &segIndex1](const auto& pt) {
+                detail::down_cast<NodableArcString*>(&e0)->addIntersection(pt, segIndex0);
+                detail::down_cast<NodableArcString*>(&e1)->addIntersection(pt, segIndex1);
+            });
+        }
+    }
 }
 
 void
diff --git a/src/noding/NodableArcString.cpp b/src/noding/NodableArcString.cpp
index 331669349..96d8e8880 100644
--- a/src/noding/NodableArcString.cpp
+++ b/src/noding/NodableArcString.cpp
@@ -14,6 +14,8 @@
 
 #include <geos/noding/NodableArcString.h>
 
+#define DEBUG_NODABLE_ARC_STRING 0
+
 namespace geos::noding {
 
 static double
@@ -34,20 +36,11 @@ NodableArcString::NodableArcString(std::vector<geom::CircularArc> arcs, std::uni
 {
 }
 
-
-//std::unique_ptr<ArcString> clone() const {
-//
-//}
-
 std::unique_ptr<ArcString>
 NodableArcString::getNoded() {
 
     auto dstSeq = std::make_unique<geom::CoordinateSequence>(0, m_constructZ, m_constructM);
 
-        //if (m_adds.empty()) {
-        //    return clone();
-        //}
-
         std::vector<geom::CircularArc> arcs;
         for (size_t i = 0; i < m_arcs.size(); i++) {
             if (const auto it = m_adds.find(i); it == m_adds.end()) {
@@ -80,7 +73,7 @@ NodableArcString::getNoded() {
                     }
                 });
 
-#if 0
+#if DEBUG_NODABLE_ARC_STRING
                 std::cout << "Splitting " << toSplit.toString() << " " << (isCCW ? "CCW" : "CW") << " paStart " << paStart << " paStop " << geom::Quadrant::pseudoAngle(center, toSplit.p2()) << std::endl;
                 for (const auto& splitPt : splitPoints)
                 {
@@ -97,7 +90,11 @@ NodableArcString::getNoded() {
 
                 // Add intermediate points of split arc
                 for (const auto& splitPoint : splitPoints) {
-                    if (!arcs.empty() && splitPoint.equals2D(arcs.back().p2())) {
+                    if (arcs.empty()) {
+                        if (splitPoint.equals2D(p0)) {
+                            continue;
+                        }
+                    } else if (splitPoint.equals2D(arcs.back().p2())) {
                         continue;
                     }
 
@@ -116,14 +113,15 @@ NodableArcString::getNoded() {
 
                 // Add last point of split arc
                 toSplit.getCoordinateSequence()->getAt(toSplit.getCoordinatePosition() + 2, p2);
+                if (arcs.empty() || !arcs.back().p2().equals2D(p2)) {
+                    geom::CoordinateXYZM midpoint(algorithm::CircularArcs::getMidpoint(p0, p2, center, radius, isCCW));
+                    midpoint.z = (p0.z + p2.z) / 2;
+                    midpoint.m = (p0.m + p2.m) / 2;
 
-                geom::CoordinateXYZM midpoint(algorithm::CircularArcs::getMidpoint(p0, p2, center, radius, isCCW));
-                midpoint.z = (p0.z + p2.z) / 2;
-                midpoint.m = (p0.m + p2.m) / 2;
-
-                dstSeq->add(midpoint);
-                dstSeq->add(p2);
-                arcs.emplace_back(*dstSeq, dstPos, center, radius, orientation);
+                    dstSeq->add(midpoint);
+                    dstSeq->add(p2);
+                    arcs.emplace_back(*dstSeq, dstPos, center, radius, orientation);
+                }
             }
         }
 
diff --git a/tests/unit/capi/GEOSNodeTest.cpp b/tests/unit/capi/GEOSNodeTest.cpp
index 3fe730d9a..f2f3d2607 100644
--- a/tests/unit/capi/GEOSNodeTest.cpp
+++ b/tests/unit/capi/GEOSNodeTest.cpp
@@ -259,5 +259,25 @@ void object::test<11>()
                                       reinterpret_cast<Geometry*>(expected_), 1e-4);
 }
 
+template<>
+template<>
+void object::test<12>()
+{
+    set_test_name("two cocircular CIRCULARSTRINGs");
+
+    input_ = fromWKT("MULTICURVE ("
+        "CIRCULARSTRING (-5 0, 0 5, 5 0),"
+        "CIRCULARSTRING (-4 3, 0 5, 4 3))");
+
+    result_ = GEOSNode(input_);
+
+    expected_ = fromWKT("MULTICURVE ("
+                        "CIRCULARSTRING (-5.0000000000000000 0.0000000000000000, -4.7434164902525691 1.5811388300841900, -4.0000000000000000 3.0000000000000000, 0.0000000000000003 5.0000000000000000, 4.0000000000000000 3.0000000000000000, 4.7434164902525691 1.5811388300841898, 5.0000000000000000 0.0000000000000000),"
+                        "CIRCULARSTRING (-4.0000000000000000 3.0000000000000000, 0.0000000000000003 5.0000000000000000, 4.0000000000000000 3.0000000000000000))");
+
+    ensure_equals_exact_geometry_xyzm(reinterpret_cast<Geometry*>(result_),
+                                      reinterpret_cast<Geometry*>(expected_), 1e-4);
+}
+
 } // namespace tut
 
diff --git a/tests/unit/noding/NodableArcStringTest.cpp b/tests/unit/noding/NodableArcStringTest.cpp
index d468b1d46..7c90e3831 100644
--- a/tests/unit/noding/NodableArcStringTest.cpp
+++ b/tests/unit/noding/NodableArcStringTest.cpp
@@ -26,7 +26,7 @@ struct test_nodablearcstring_data {
         NodableArcString nas(arcs, nullptr, false, false, nullptr);
 
         for (const auto& coord : coords) {
-            nas.addInt(coord, 0);
+            nas.addIntersection(coord, 0);
         }
 
         auto noded = nas.getNoded();
diff --git a/tests/unit/noding/SimpleNoderTest.cpp b/tests/unit/noding/SimpleNoderTest.cpp
index 401763bef..06ffb07e2 100644
--- a/tests/unit/noding/SimpleNoderTest.cpp
+++ b/tests/unit/noding/SimpleNoderTest.cpp
@@ -25,18 +25,10 @@ namespace tut {
 
 struct test_simplenoder_data {
 
-    // FIXME: This is duplicated from CircularArcIntersectorTest
     template<typename T>
     CircularArc makeArc(T p0, T p2, const CoordinateXY& center, double radius, int orientation)
     {
-        auto seq = std::make_unique<CoordinateSequence>(3, T::template has<Ordinate::Z>(), T::template has<Ordinate::M>());
-        seq->setAt(p0, 0);
-        seq->setAt(geos::algorithm::CircularArcs::getMidpoint(p0, p2, center, radius, orientation == Orientation::COUNTERCLOCKWISE), 1);
-        seq->setAt(p2, 2);
-
-        CircularArc ret(std::move(seq), 0);
-
-        return ret;
+        return CircularArc::create(p0, p2, center, radius, orientation);
     }
 
     template<typename T1, typename T2>

commit 4e0f6f89ba90a1940166537cc9449a4bd34e9622
Author: Daniel Baston <dbaston at gmail.com>
Date:   Mon Dec 15 10:00:53 2025 -0500

    CircularArcIntersector: Improve endpoint calculation for cocircular arcs

diff --git a/src/algorithm/CircularArcIntersector.cpp b/src/algorithm/CircularArcIntersector.cpp
index 8f0c5dacb..ae9e85758 100644
--- a/src/algorithm/CircularArcIntersector.cpp
+++ b/src/algorithm/CircularArcIntersector.cpp
@@ -507,7 +507,6 @@ CircularArcIntersector::intersects(const CircularArc& arc1, const CircularArc& a
         }
 #endif
 
-#if 1
         // Add endpoint intersections missed due to precision issues.
         // TODO: Add some logic to prevent double-counting of endpoints. Ideally, the endpoint test would happen before
         // computing intersection points, so if there is an endpoint intersection we get the exact intersection point
@@ -524,7 +523,6 @@ CircularArcIntersector::intersects(const CircularArc& arc1, const CircularArc& a
         if (nPt < 2 && arc1.p2().equals2D(arc2.p2()) && (nPt == 0 || !intPt[0].equals2D(arc1.p2()))) {
             addIntersection(arc1.p2(), arc1, arc2);
         }
-#endif
     }
 
     if (nArc) {
@@ -566,6 +564,17 @@ CircularArcIntersector::intersects(const CoordinateSequence &p, std::size_t p0,
     }
 }
 
+static void
+setFromEndpoint(geom::CoordinateXYZM& pt, const CircularArc& arc, std::size_t index)
+{
+    arc.applyAt(index, [&pt](const auto& endpoint) {
+        pt.x = endpoint.x;
+        pt.y = endpoint.y;
+        pt.z = Interpolate::zGet(pt, endpoint);
+        pt.m = Interpolate::mGet(pt, endpoint);
+    });
+}
+
 void
 CircularArcIntersector::addArcIntersection(double startAngle, double endAngle, int orientation, const CircularArc& arc1, const CircularArc& arc2)
 {
@@ -580,48 +589,27 @@ CircularArcIntersector::addArcIntersection(double startAngle, double endAngle, i
     CoordinateXYZM computedMidPt(CircularArcs::createPoint(center, radius, theta1));
     CoordinateXYZM computedEndPt(CircularArcs::createPoint(center, radius, endAngle));
 
-    if (computedStartPt.equals2D(arc1.p0())) {
-        arc1.applyAt(0, [&computedStartPt](const auto& endpoint) {
-            computedStartPt.z = Interpolate::zGet(computedStartPt, endpoint);
-            computedStartPt.m = Interpolate::mGet(computedStartPt, endpoint);
-        });
-    } else if (computedStartPt.equals2D(arc1.p2())) {
-        arc1.applyAt(2, [&computedStartPt](const auto& endpoint) {
-            computedStartPt.z = Interpolate::zGet(computedStartPt, endpoint);
-            computedStartPt.m = Interpolate::mGet(computedStartPt, endpoint);
-        });
-    } else if (computedStartPt.equals2D(arc2.p0())) {
-        arc2.applyAt(0, [&computedStartPt](const auto& endpoint) {
-            computedStartPt.z = Interpolate::zGet(computedStartPt, endpoint);
-            computedStartPt.m = Interpolate::mGet(computedStartPt, endpoint);
-        });
-    } else if (computedStartPt.equals2D(arc2.p2())) {
-        arc2.applyAt(2, [&computedStartPt](const auto& endpoint) {
-            computedStartPt.z = Interpolate::zGet(computedStartPt, endpoint);
-            computedStartPt.m = Interpolate::mGet(computedStartPt, endpoint);
-        });
+    // Check to see if the endpoints of the intersection match the endpoints of either of
+    // the endpoints. Use angles for the check to avoid missing an endpoint intersection from
+    // inaccuracy the point construction.
+    if (startAngle == arc1.theta0()) {
+        setFromEndpoint(computedStartPt, arc1, 0);
+    } else if (startAngle == arc1.theta2()) {
+        setFromEndpoint(computedStartPt, arc1, 2);
+    } else if (startAngle == arc2.theta0()) {
+        setFromEndpoint(computedStartPt, arc2, 0);
+    } else if (startAngle == arc2.theta2()) {
+        setFromEndpoint(computedStartPt, arc2, 2);
     }
 
-    if (computedEndPt.equals2D(arc1.p0())) {
-        arc1.applyAt(0, [&computedEndPt](const auto& endpoint) {
-            computedEndPt.z = Interpolate::zGet(computedEndPt, endpoint);
-            computedEndPt.m = Interpolate::mGet(computedEndPt, endpoint);
-        });
-    } else if (computedEndPt.equals2D(arc1.p2())) {
-        arc1.applyAt(2, [&computedEndPt](const auto& endpoint) {
-            computedEndPt.z = Interpolate::zGet(computedEndPt, endpoint);
-            computedEndPt.m = Interpolate::mGet(computedEndPt, endpoint);
-        });
-    } else if (computedEndPt.equals2D(arc2.p0())) {
-        arc2.applyAt(0, [&computedEndPt](const auto& endpoint) {
-            computedEndPt.z = Interpolate::zGet(computedEndPt, endpoint);
-            computedEndPt.m = Interpolate::mGet(computedEndPt, endpoint);
-        });
-    } else if (computedEndPt.equals2D(arc2.p2())) {
-        arc2.applyAt(2, [&computedEndPt](const auto& endpoint) {
-            computedEndPt.z = Interpolate::zGet(computedEndPt, endpoint);
-            computedEndPt.m = Interpolate::mGet(computedEndPt, endpoint);
-        });
+    if (endAngle == arc1.theta0()) {
+        setFromEndpoint(computedEndPt, arc1, 0);
+    } else if (endAngle == arc1.theta2()) {
+        setFromEndpoint(computedEndPt, arc1, 2);
+    } else if (endAngle == arc2.theta0()) {
+        setFromEndpoint(computedEndPt, arc2, 0);
+    } else if (endAngle == arc2.theta2()) {
+        setFromEndpoint(computedEndPt, arc2, 2);
     }
 
     interpolateZM(arc1, arc2, computedStartPt);

commit bac74b25b9508a54a14fceb6bb9ed172ad10e3b4
Author: Daniel Baston <dbaston at gmail.com>
Date:   Mon Nov 17 13:32:34 2025 -0500

    GEOSNode: support arcs

diff --git a/include/geos/algorithm/CircularArcIntersector.h b/include/geos/algorithm/CircularArcIntersector.h
index c7a8c323b..c0aa3b753 100644
--- a/include/geos/algorithm/CircularArcIntersector.h
+++ b/include/geos/algorithm/CircularArcIntersector.h
@@ -3,7 +3,7 @@
  * GEOS - Geometry Engine Open Source
  * http://geos.osgeo.org
  *
- * Copyright (C) 2024 ISciences, LLC
+ * Copyright (C) 2024-2025 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
@@ -18,15 +18,16 @@
 #include <cstdint>
 
 #include <geos/export.h>
+#include <geos/algorithm/LineIntersector.h>
 #include <geos/geom/Coordinate.h>
 #include <geos/geom/CircularArc.h>
-#include <geos/geom/LineSegment.h>
 
 namespace geos::algorithm {
 
 class GEOS_DLL CircularArcIntersector {
 public:
     using CoordinateXY = geom::CoordinateXY;
+    using CoordinateXYZM = geom::CoordinateXYZM;
     using CircularArc = geom::CircularArc;
     using Envelope = geom::Envelope;
 
@@ -42,7 +43,7 @@ public:
         return result;
     }
 
-    const CoordinateXY& getPoint(std::uint8_t i) const
+    const CoordinateXYZM& getPoint(std::uint8_t i) const
     {
         return intPt[i];
     }
@@ -62,28 +63,33 @@ public:
         return nArc;
     }
 
-    /// Determines whether and where a circular arc intersects a line segment.
-    ///
-    /// Sets the appropriate value of intersection_type and stores the intersection
-    /// points, if any.
-    void intersects(const CircularArc& arc, const CoordinateXY& p0, const CoordinateXY& p1);
+    /** Determines whether and where a circular arc intersects a line segment.
+     *
+     * Sets the appropriate value of intersection_type and stores the intersection points, if any.
+     *
+     * @param arc The circular arc
+     * @param seq A CoordinateSequence containing the points of the line segment
+     * @param pos0 The index of the first point in the line segment
+     * @param pos1 The index of the second point in the line segment
+     * @param useSegEndpoints Whether to preferentially take Z/M values from
+     *        the endpoints of the line segment rather than the arc.
+     */
+    void intersects(const CircularArc& arc, const geom::CoordinateSequence& seq, std::size_t pos0, std::size_t pos1, bool useSegEndpoints);
 
-    void intersects(const CircularArc& arc, const geom::LineSegment& seg)
-    {
-        intersects(arc, seg.p0, seg.p1);
-    }
-
-    /// Determines whether and where two circular arcs intersect.
-    ///
-    ///	Sets the appropriate value of intersection_type and stores the intersection
-    /// points and/or arcs, if any.
+    /** Determines whether and where two circular arcs intersect.
+     *
+     * 	Sets the appropriate value of intersection_type and stores the intersection
+     *  points and/or arcs, if any.
+     */
     void intersects(const CircularArc& arc1, const CircularArc& arc2);
 
-    static int
-    circleIntersects(const CoordinateXY& center, double r, const CoordinateXY& p0, const CoordinateXY& p1, CoordinateXY& isect0, CoordinateXY& isect1);
-
-
-    void intersects(const CoordinateXY& p0, const CoordinateXY& p1, const CoordinateXY& q0, const CoordinateXY& q1);
+    /** Determines whether and where two line segments intersect
+     *
+     * 	Sets the appropriate value of intersection_type and stores the intersection
+     *  points, if any.
+     */
+    void intersects(const geom::CoordinateSequence& p, std::size_t p0, std::size_t p1,
+                    const geom::CoordinateSequence& q, std::size_t q0, std::size_t q1);
 
 private:
     void reset() {
@@ -91,12 +97,52 @@ private:
         nArc = 0;
     }
 
-    std::array<CoordinateXY, 2> intPt;
+    /** Add an arc intersection of two cocircular arcs between the specified angles.
+     *
+     * The input arcs are provided so that Z/M values can be assigned to the created arc.
+     * When the endpoints of the new arc correspond with those of the inputs, Z/M values
+     * will be preferentially taken from arc1.
+     */
+    void addArcIntersection(double startAngle, double endAngle, int orientation, const CircularArc& arc1, const CircularArc& arc2);
+
+    /** Add a point intersection between two arcs.
+     *
+     * The input arcs are provided so that Z/M values can be assigned to the intersection point.
+     * When the intersection point corresponds matches one of the input arc endpoints,
+     * Z/M values will be taken from that endpoint, with arc1 having priority over arc2.
+     * If the intersection point does not equal the endpoint of either arc, its Z/M values
+     * will be interpolated.
+     */
+    void addIntersection(const CoordinateXY& computedIntPt, const CircularArc& arc1, const CircularArc& arc2);
+
+    /** Add a point intersection between an arc and a segment.
+     *
+     * The input arc and segment are provided so that Z/M values can be assigned to the intersection point.
+     * When the intersection point corresponds to the arc or segment endpoints, Z/M values
+     * will be taken from that endpoint. Priority will be given to the arc endpoints unless
+     * `useSegEndpoint` is true. If the intersection point does not equal the endpoint of the arc
+     * or the segment, its Z/M values will be interpolated.
+     */
+    void addIntersection(const CoordinateXY& computedIntPt, const CircularArc& lhs, const geom::CoordinateSequence& seq, std::size_t pos0, std::size_t pos1, bool useSegEndpoints);
+
+    /** Determines whether and where two circles intersect a line segment.
+     *
+     * @param center The center point of the circle
+     * @param r The radius of the circle
+     * @param p0 The first point of the line segment
+     * @param p1 The second point of the line segment
+     * @param isect0 Set to the first intersection point, if it exists
+     * @param isect1 Set to the second intersection point, if it exists
+     * @return The number of intersection points
+     */
+    static int
+    circleIntersects(const CoordinateXY& center, double r, const CoordinateXY& p0, const CoordinateXY& p1, CoordinateXY& isect0, CoordinateXY& isect1);
+
+    std::array<CoordinateXYZM, 2> intPt;
     std::array<CircularArc, 2> intArc;
     intersection_type result = NO_INTERSECTION;
     std::uint8_t nPt = 0;
     std::uint8_t nArc = 0;
-
 };
 
 }
diff --git a/include/geos/algorithm/Interpolate.h b/include/geos/algorithm/Interpolate.h
index 1768c89a1..aa6276b7e 100644
--- a/include/geos/algorithm/Interpolate.h
+++ b/include/geos/algorithm/Interpolate.h
@@ -160,7 +160,7 @@ public:
         return get<geom::Ordinate::M>(p, q);
     }
 
-    /// Return a coordinates's non-NaN Z value or interpolate it from two other coordinates if it is NaN.
+    /// Return a coordinate's non-NaN Z value or interpolate it from two other coordinates if it is NaN.
     template<typename C1, typename C2>
     static double
     zGetOrInterpolate(const C1& p, const C2& p1, const C2& p2)
@@ -168,7 +168,7 @@ public:
         return getOrInterpolate<geom::Ordinate::Z>(p, p1, p2);
     }
 
-    /// Return a coordinates's non-NaN M value or interpolate it from two other coordinates if it is NaN.
+    /// Return a coordinate's non-NaN M value or interpolate it from two other coordinates if it is NaN.
     template<typename C1, typename C2>
     static double
     mGetOrInterpolate(const C1& p, const C2& p1, const C2& p2)
@@ -176,6 +176,21 @@ public:
         return getOrInterpolate<geom::Ordinate::M>(p, p1, p2);
     }
 
+    static double
+    getOrAverage(double a, double b)
+    {
+        if (std::isnan(a)) {
+            return b;
+        }
+
+        if (std::isnan(b)) {
+            return a;
+        }
+
+        return 0.5*(a+b);
+    }
+
+
 };
 
 }
diff --git a/include/geos/algorithm/LineIntersector.h b/include/geos/algorithm/LineIntersector.h
index ebf676b10..76d78d83e 100644
--- a/include/geos/algorithm/LineIntersector.h
+++ b/include/geos/algorithm/LineIntersector.h
@@ -111,7 +111,7 @@ public:
      * @return <code>true</code> if either intersection point is in
      * the interior of the input segment
      */
-    bool isInteriorIntersection(std::size_t inputLineIndex)
+    bool isInteriorIntersection(std::size_t inputLineIndex) const
     {
         for(std::size_t i = 0; i < result; ++i) {
             if(!(intPt[i].equals2D(*inputLines[inputLineIndex][0])
@@ -161,6 +161,10 @@ public:
     void computeIntersection(const geom::CoordinateSequence& p, std::size_t p0,
                              const geom::CoordinateSequence& q, std::size_t q0);
 
+    /// Compute the intersection between two segments, given a sequence and indices of each endpoint
+    void computeIntersection(const geom::CoordinateSequence& p, std::size_t p0, std::size_t p1,
+                             const geom::CoordinateSequence& q, std::size_t q0, std::size_t q1);
+
     std::string toString() const;
 
     /**
diff --git a/include/geos/algorithm/RayCrossingCounter.h b/include/geos/algorithm/RayCrossingCounter.h
index 2120e2358..2bdbf8c39 100644
--- a/include/geos/algorithm/RayCrossingCounter.h
+++ b/include/geos/algorithm/RayCrossingCounter.h
@@ -113,9 +113,7 @@ public:
     void countSegment(const geom::CoordinateXY& p1,
                       const geom::CoordinateXY& p2);
 
-    void countArc(const geom::CoordinateXY& p1,
-                  const geom::CoordinateXY& p2,
-                  const geom::CoordinateXY& p3);
+    void countArc(const geom::CircularArc& arc);
 
     /** \brief
      *  Counts all segments or arcs in the sequence
diff --git a/include/geos/geom/CircularArc.h b/include/geos/geom/CircularArc.h
index be2b2fe43..184b07f46 100644
--- a/include/geos/geom/CircularArc.h
+++ b/include/geos/geom/CircularArc.h
@@ -3,7 +3,7 @@
  * GEOS - Geometry Engine Open Source
  * http://geos.osgeo.org
  *
- * Copyright (C) 2024 ISciences, LLC
+ * Copyright (C) 2024-2025 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
@@ -20,7 +20,6 @@
 #include <geos/geom/Quadrant.h>
 #include <geos/algorithm/CircularArcs.h>
 #include <geos/algorithm/Orientation.h>
-#include <geos/triangulate/quadedge/TrianglePredicate.h>
 
 namespace geos {
 namespace geom {
@@ -30,46 +29,80 @@ namespace geom {
 class GEOS_DLL CircularArc {
 public:
 
-    using CoordinateXY = geom::CoordinateXY;
+    /// Create an empty CircularArc backed by a null CoordinateSequence. A CircularArc created in this way will
+    /// crash if methods are called on it, but a default constructor is necessary to construct std::array<CircularArc, >
+    CircularArc();
 
-    CircularArc() : CircularArc({0, 0}, {0, 0}, {0, 0}) {}
+    /// Create a CircularArc that refers to points in the provided CoordinateSequence. The CoordinateSequence is not
+    /// owned by the CircularArc and should remain reachable for the lifetime of the CircularArc.
+    /// The center, radius, and orientation can be specified to avoid lossy re-computation. They are not checked for
+    /// consistency with the references coordinates.
+    CircularArc(const CoordinateSequence&, std::size_t pos);
+    CircularArc(const CoordinateSequence&, std::size_t pos, const CoordinateXY& center, double radius, int orientation);
 
-    CircularArc(const CoordinateXY& q0, const CoordinateXY& q1, const CoordinateXY& q2)
-        : p0(q0)
-        , p1(q1)
-        , p2(q2)
-        , m_center_known(false)
-        , m_radius_known(false)
-        , m_orientation_known(false)
-    {}
+    /// Create a CircularArc that refers to points in the provided CoordinateSequence.
+    /// Ownership of the CoordinateSequence is transferred to the CircularArc.
+    /// The center, radius, and orientation can be specified to avoid lossy re-computation. They are not checked for
+    /// consistency with the references coordinates.
+    CircularArc(std::unique_ptr<CoordinateSequence>, std::size_t pos);
+    CircularArc(std::unique_ptr<CoordinateSequence>, std::size_t pos, const CoordinateXY& center, double radius, int orientation);
 
-    CircularArc(double theta0, double theta2, const CoordinateXY& center, double radius, int orientation)
-        : p0(algorithm::CircularArcs::createPoint(center, radius, theta0)),
-          p1(algorithm::CircularArcs::createPoint(center, radius, algorithm::CircularArcs::getMidpointAngle(theta0, theta2, orientation==algorithm::Orientation::COUNTERCLOCKWISE))),
-          p2(algorithm::CircularArcs::createPoint(center, radius, theta2)),
-          m_center(center),
-          m_radius(radius),
-          m_orientation(orientation),
-          m_center_known(true),
-          m_radius_known(true),
-          m_orientation_known(true)
-    {}
+    CircularArc(const CircularArc& other);
 
-    CircularArc(const CoordinateXY& q0, const CoordinateXY& q2, const CoordinateXY& center, double radius, int orientation)
-        : p0(q0),
-          p1(algorithm::CircularArcs::getMidpoint(q0, q2, center, radius, orientation==algorithm::Orientation::COUNTERCLOCKWISE)),
-          p2(q2),
-          m_center(center),
-          m_radius(radius),
-          m_orientation(orientation),
-          m_center_known(true),
-          m_radius_known(true),
-          m_orientation_known(true)
-    {}
+    CircularArc(CircularArc&&) noexcept;
 
-    CoordinateXY p0;
-    CoordinateXY p1;
-    CoordinateXY p2;
+    CircularArc& operator=(const CircularArc& other);
+    CircularArc& operator=(CircularArc&&) noexcept;
+
+    ~CircularArc();
+
+    /// Create a CircularArc from the given coordinates, automatically allocating a CoordinateSequence to store them.
+    /// Typically, it is more efficient to create a larger CoordinateSequence that multiple CircularArcs refer to.
+    /// However, the "create" methods are convenient for writing tests.
+    template<typename CoordType>
+    static CircularArc create(const CoordType& p0, const CoordType& p1, const CoordType& p2)
+    {
+        auto seq = std::make_unique<CoordinateSequence>(3, CoordType::template has<Ordinate::Z>(), CoordType::template has<Ordinate::M>());
+        seq->setAt(p0, 0);
+        seq->setAt(p1, 1);
+        seq->setAt(p2, 2);
+
+        CircularArc ret(std::move(seq), 0);
+
+        return ret;
+    }
+
+    static CircularArc create(const CoordinateXY& p0, const CoordinateXY& p2, const CoordinateXY& center, double radius, int orientation);
+    static CircularArc create(const Coordinate& p0, const Coordinate& p2, const CoordinateXY& center, double radius, int orientation);
+    static CircularArc create(const CoordinateXYM& p0, const CoordinateXYM& p2, const CoordinateXY& center, double radius, int orientation);
+    static CircularArc create(const CoordinateXYZM& p0, const CoordinateXYZM& p2, const CoordinateXY& center, double radius, int orientation);
+
+    /// Return the inner angle of the sector associated with this arc
+    double getAngle() const;
+
+    /// Return the area enclosed by the arc p0-p1-p2 and the line segment p2-p0
+    double getArea() const;
+
+    /// Return the center point of the circle associated with this arc
+    const CoordinateXY& getCenter() const {
+        if (!m_center_known) {
+            m_center = algorithm::CircularArcs::getCenter(p0(), p1(), p2());
+            m_center_known = true;
+        }
+
+        return m_center;
+    }
+
+    const CoordinateSequence* getCoordinateSequence() const {
+        return m_seq;
+    }
+
+    std::size_t getCoordinatePosition() const {
+        return m_pos;
+    }
+
+    /// Return the length of the arc
+    double getLength() const;
 
     /// Return the orientation of the arc as one of:
     /// - algorithm::Orientation::CLOCKWISE,
@@ -77,39 +110,35 @@ public:
     /// - algorithm::Orientation::COLLINEAR
     int getOrientation() const {
         if (!m_orientation_known) {
-            m_orientation = algorithm::Orientation::index(p0, p1, p2);
+            m_orientation = algorithm::Orientation::index(p0(), p1(), p2());
             m_orientation_known = true;
         }
         return m_orientation;
     }
 
-    bool isCCW() const {
-        return getOrientation() == algorithm::Orientation::COUNTERCLOCKWISE;
-    }
-
-    /// Return the center point of the circle associated with this arc
-    const CoordinateXY& getCenter() const {
-        if (!m_center_known) {
-            m_center = algorithm::CircularArcs::getCenter(p0, p1, p2);
-            m_center_known = true;
-        }
-
-        return m_center;
-    }
-
     /// Return the radius of the circle associated with this arc
     double getRadius() const {
         if (!m_radius_known) {
-            m_radius = getCenter().distance(p0);
+            m_radius = getCenter().distance(p0());
             m_radius_known = true;
         }
 
         return m_radius;
     }
 
+    /// Return the distance from the centerpoint of the arc to the line segment formed by the end points of the arc.
+    double getSagitta() const {
+        CoordinateXY midpoint = algorithm::CircularArcs::getMidpoint(p0(), p2(), getCenter(), getRadius(), isCCW());
+        return algorithm::Distance::pointToSegment(midpoint, p0(), p2());
+    }
+
+    bool isCCW() const {
+        return getOrientation() == algorithm::Orientation::COUNTERCLOCKWISE;
+    }
+
     /// Return whether this arc forms a complete circle
     bool isCircle() const {
-        return p0.equals(p2);
+        return p0().equals(p2());
     }
 
     /// Returns whether this arc forms a straight line (p0, p1, and p2 are collinear)
@@ -117,68 +146,26 @@ public:
         return !std::isfinite(getRadius());
     }
 
-    /// Return the inner angle of the sector associated with this arc
-    double getAngle() const {
-        if (isCircle()) {
-            return 2*MATH_PI;
-        }
-
-        /// Even Rouault:
-        /// potential optimization?: using crossproduct(p0 - center, p2 - center) = radius * radius * sin(angle)
-        /// could yield the result by just doing a single asin(), instead of 2 atan2()
-        /// actually one should also likely compute dotproduct(p0 - center, p2 - center) = radius * radius * cos(angle),
-        /// and thus angle = atan2(crossproduct(p0 - center, p2 - center) , dotproduct(p0 - center, p2 - center) )
-        auto t0 = theta0();
-        auto t2 = theta2();
-
-        if (getOrientation() == algorithm::Orientation::COUNTERCLOCKWISE) {
-            std::swap(t0, t2);
-        }
-
-        if (t0 < t2) {
-            t0 += 2*MATH_PI;
-        }
-
-        auto diff = t0-t2;
-
-        return diff;
-    }
-
-    /// Return the length of the arc
-    double getLength() const {
-        if (isLinear()) {
-            return p0.distance(p2);
-        }
-
-        return getAngle()*getRadius();
-    }
-
-    /// Return the area enclosed by the arc p0-p1-p2 and the line segment p2-p0
-    double getArea() const {
-        if (isLinear()) {
-            return 0;
-        }
-
-        auto R = getRadius();
-        auto theta = getAngle();
-        return R*R/2*(theta - std::sin(theta));
-    }
-
-    /// Return the distance from the centerpoint of the arc to the line segment formed by the end points of the arc.
-    double getSagitta() const {
-        CoordinateXY midpoint = algorithm::CircularArcs::getMidpoint(p0, p2, getCenter(), getRadius(), isCCW());
-        return algorithm::Distance::pointToSegment(midpoint, p0, p2);
-    }
-
     /// Return the angle of p0
     double theta0() const {
-        return algorithm::CircularArcs::getAngle(p0, getCenter());
+        return algorithm::CircularArcs::getAngle(p0(), getCenter());
+    }
+
+    /// Return the angle of p1
+    double theta1() const {
+        return algorithm::CircularArcs::getAngle(p1(), getCenter());
     }
 
     /// Return the angle of p2
     double theta2() const {
-        return algorithm::CircularArcs::getAngle(p2, getCenter());
+        return algorithm::CircularArcs::getAngle(p2(), getCenter());
     }
+    /// Check to see if a given angle lies on this arc
+    bool containsAngle(double theta) const;
+
+    /// Check to see if a coordinate lies on the arc, after testing whether
+    /// it lies on the circle.
+    bool containsPoint(const CoordinateXY& q) const;
 
     /// Check to see if a coordinate lies on the arc
     /// Only the angle is checked, so it is assumed that the point lies on
@@ -188,91 +175,18 @@ public:
         return containsAngle(theta);
     }
 
-    /// Check to see if a coordinate lies on the arc, after testing whether
-    /// it lies on the circle.
-    bool containsPoint(const CoordinateXY& q) const {
-        if (q == p0 || q == p1 || q == p2) {
-            return true;
-        }
-
-        //auto dist = std::abs(q.distance(getCenter()) - getRadius());
-
-        //if (dist > 1e-8) {
-        //    return false;
-        //}
-
-        if (triangulate::quadedge::TrianglePredicate::isInCircleRobust(p0, p1, p2, q) != geom::Location::BOUNDARY) {
-            return false;
-        }
-
-        return containsPointOnCircle(q);
-    }
-
-    /// Check to see if a given angle lies on this arc
-    bool containsAngle(double theta) const {
-        auto t0 = theta0();
-        auto t2 = theta2();
-
-        if (theta == t0 || theta == t2) {
-            return true;
-        }
-
-        if (getOrientation() == algorithm::Orientation::COUNTERCLOCKWISE) {
-            std::swap(t0, t2);
-        }
-
-        t2 -= t0;
-        theta -= t0;
-
-        if (t2 < 0) {
-            t2 += 2*MATH_PI;
-        }
-        if (theta < 0) {
-            theta += 2*MATH_PI;
-        }
-
-        return theta >= t2;
-    }
-
     /// Return true if the arc is pointing positive in the y direction
     /// at the location of a specified point. The point is assumed to
     /// be on the arc.
-    bool isUpwardAtPoint(const CoordinateXY& q) const {
-        auto quad = geom::Quadrant::quadrant(getCenter(), q);
-        bool isUpward;
+    bool isUpwardAtPoint(const CoordinateXY& q) const;
 
-        if (getOrientation() == algorithm::Orientation::CLOCKWISE) {
-            isUpward = (quad == geom::Quadrant::SW || quad == geom::Quadrant::NW);
-        } else {
-            isUpward = (quad == geom::Quadrant::SE || quad == geom::Quadrant::NE);
-        }
-
-        return isUpward;
-    }
-
-    void reverse() {
-        std::swap(p0, p2);
-        if (m_orientation_known) {
-            if (m_orientation == algorithm::Orientation::COUNTERCLOCKWISE) {
-                m_orientation = algorithm::Orientation::CLOCKWISE;
-            } else if (m_orientation == algorithm::Orientation::CLOCKWISE) {
-                m_orientation = algorithm::Orientation::COUNTERCLOCKWISE;
-            }
-        }
-    }
+    CircularArc reverse() const;
 
     // Split an arc at a specified point.
-    // The point is assumed to be o the arc.
-    std::pair<CircularArc, CircularArc> splitAtPoint(const CoordinateXY& q) const {
-        return {
-            CircularArc(p0, q, getCenter(), getRadius(), getOrientation()),
-            CircularArc(q, p2, getCenter(), getRadius(), getOrientation())
-        };
-    }
+    // The point is assumed to be on the arc.
+    //std::pair<CircularArc, CircularArc> splitAtPoint(const CoordinateXY& q) const;
 
-    std::string toString() const {
-        return "CIRCULARSTRING (" + p0.toString() + ", " + p1.toString() + ", " + p2.toString() + ")";
-    }
+    bool equals(const CircularArc& other, double tol) const;
 
     class Iterator {
     public:
@@ -285,7 +199,7 @@ public:
         Iterator(const CircularArc& arc, int i) : m_arc(arc), m_i(i) {}
 
         reference operator*() const {
-            return m_i == 0 ? m_arc.p0 : (m_i == 1 ? m_arc.p1 : m_arc.p2);
+            return m_i == 0 ? m_arc.p0() : (m_i == 1 ? m_arc.p1() : m_arc.p2());
         }
 
         Iterator& operator++() {
@@ -321,13 +235,39 @@ public:
         return Iterator(*this, 3);
     }
 
+    template<typename T=CoordinateXY>
+    const T& p0() const {
+        return m_seq->getAt<T>(m_pos);
+    }
+
+    template<typename T=CoordinateXY>
+    const T& p1() const {
+        return m_seq->getAt<T>(m_pos + 1);
+    }
+
+    template<typename T=CoordinateXY>
+    const T& p2() const {
+        return m_seq->getAt<T>(m_pos + 2);
+    }
+
+    std::string toString() const;
+
+    template<typename F>
+    auto applyAt(std::size_t i, F&& f) const {
+        return m_seq->applyAt(m_pos + i, f);
+    }
+
 private:
+    const CoordinateSequence* m_seq;
+    std::size_t m_pos;
+
     mutable CoordinateXY m_center;
     mutable double m_radius;
     mutable int m_orientation;
     mutable bool m_center_known = false;
     mutable bool m_radius_known = false;
     mutable bool m_orientation_known = false;
+    bool m_own_coordinates;
 };
 
 }
diff --git a/include/geos/geom/Coordinate.h b/include/geos/geom/Coordinate.h
index 0d8bced0f..ad0d84530 100644
--- a/include/geos/geom/Coordinate.h
+++ b/include/geos/geom/Coordinate.h
@@ -78,6 +78,9 @@ public:
     template<Ordinate>
     double get() const;
 
+    template<Ordinate>
+    static constexpr bool has();
+
     /// x-coordinate
     double x;
 
@@ -255,6 +258,9 @@ public:
         , z(DEFAULT_Z)
         {};
 
+    template<Ordinate>
+    static constexpr bool has();
+
     template<Ordinate>
     double get() const;
 
@@ -308,6 +314,9 @@ public:
 
     double m;
 
+    template<Ordinate>
+    static constexpr bool has();
+
     template<Ordinate>
     double get() const;
 
@@ -366,6 +375,9 @@ public:
 
     double m;
 
+    template<Ordinate>
+    static constexpr bool has();
+
     template<Ordinate>
     double get() const;
 
@@ -463,6 +475,30 @@ inline bool operator<(const CoordinateXY& a, const CoordinateXY& b)
 
 // Generic accessors, XY
 
+template<>
+constexpr bool CoordinateXY::has<Ordinate::X>()
+{
+    return true;
+}
+
+template<>
+constexpr bool CoordinateXY::has<Ordinate::Y>()
+{
+    return true;
+}
+
+template<>
+constexpr bool CoordinateXY::has<Ordinate::Z>()
+{
+    return false;
+}
+
+template<>
+constexpr bool CoordinateXY::has<Ordinate::M>()
+{
+    return false;
+}
+
 template<>
 inline double CoordinateXY::get<Ordinate::X>() const
 {
@@ -489,6 +525,30 @@ inline double CoordinateXY::get<Ordinate::M>() const
 
 // Generic accessors, XYZ
 
+template<>
+constexpr bool Coordinate::has<Ordinate::X>()
+{
+    return true;
+}
+
+template<>
+constexpr bool Coordinate::has<Ordinate::Y>()
+{
+    return true;
+}
+
+template<>
+constexpr bool Coordinate::has<Ordinate::Z>()
+{
+    return true;
+}
+
+template<>
+constexpr bool Coordinate::has<Ordinate::M>()
+{
+    return false;
+}
+
 template<>
 inline double Coordinate::get<Ordinate::X>() const
 {
@@ -515,6 +575,30 @@ inline double Coordinate::get<Ordinate::M>() const
 
 // Generic accessors, XYM
 
+template<>
+constexpr bool CoordinateXYM::has<Ordinate::X>()
+{
+    return true;
+}
+
+template<>
+constexpr bool CoordinateXYM::has<Ordinate::Y>()
+{
+    return true;
+}
+
+template<>
+constexpr bool CoordinateXYM::has<Ordinate::Z>()
+{
+    return false;
+}
+
+template<>
+constexpr bool CoordinateXYM::has<Ordinate::M>()
+{
+    return true;
+}
+
 template<>
 inline double CoordinateXYM::get<Ordinate::X>() const
 {
@@ -541,6 +625,30 @@ inline double CoordinateXYM::get<Ordinate::M>() const
 
 // Generic accessors, XYZM
 
+template<>
+constexpr bool CoordinateXYZM::has<Ordinate::X>()
+{
+    return true;
+}
+
+template<>
+constexpr bool CoordinateXYZM::has<Ordinate::Y>()
+{
+    return true;
+}
+
+template<>
+constexpr bool CoordinateXYZM::has<Ordinate::Z>()
+{
+    return true;
+}
+
+template<>
+constexpr bool CoordinateXYZM::has<Ordinate::M>()
+{
+    return true;
+}
+
 template<>
 inline double CoordinateXYZM::get<Ordinate::X>() const
 {
diff --git a/include/geos/geom/CoordinateSequence.h b/include/geos/geom/CoordinateSequence.h
index e4a1c5553..249e9ddd7 100644
--- a/include/geos/geom/CoordinateSequence.h
+++ b/include/geos/geom/CoordinateSequence.h
@@ -681,6 +681,8 @@ public:
 
     void sort();
 
+    /// Swap the coordinates at two indices.
+    void swap(std::size_t i, std::size_t j);
 
     /**
      * Expands the given Envelope to include the coordinates in the
diff --git a/include/geos/noding/ArcNoder.h b/include/geos/noding/ArcNoder.h
index 4337db295..2fd418cfa 100644
--- a/include/geos/noding/ArcNoder.h
+++ b/include/geos/noding/ArcNoder.h
@@ -27,11 +27,17 @@ namespace geos::noding {
 class GEOS_DLL ArcNoder : public Noder {
 
 public:
+    ArcNoder() = default;
+
     explicit ArcNoder(std::unique_ptr<ArcIntersector> intersector) :
         m_intersector(std::move(intersector)) {}
 
     ~ArcNoder() override;
 
+    void setArcIntersector(std::unique_ptr<ArcIntersector> arcIntersector) {
+        m_intersector = std::move(arcIntersector);
+    }
+
     void computeNodes(const std::vector<SegmentString*>& segStrings) override;
 
     std::vector<std::unique_ptr<SegmentString>> getNodedSubstrings() override;
diff --git a/include/geos/noding/ArcString.h b/include/geos/noding/ArcString.h
index 10f254ed2..c442a26f6 100644
--- a/include/geos/noding/ArcString.h
+++ b/include/geos/noding/ArcString.h
@@ -32,6 +32,12 @@ public:
     explicit ArcString(std::vector<geom::CircularArc> arcs) : m_arcs(std::move(arcs)) {
     }
 
+    ArcString(std::vector<geom::CircularArc> arcs, std::unique_ptr<geom::CoordinateSequence> seq, void* context)
+        : m_arcs(std::move(arcs)),
+          m_seq(std::move(seq)),
+          m_context(context)
+    {}
+
     std::size_t getSize() const override {
         return m_arcs.size();
     }
@@ -56,8 +62,12 @@ public:
         return m_arcs.end();
     }
 
+    std::unique_ptr<geom::CoordinateSequence> releaseCoordinates();
+
 protected:
     std::vector<geom::CircularArc> m_arcs;
+    std::unique_ptr<geom::CoordinateSequence> m_seq;
+    void* m_context;
 };
 
 }
\ No newline at end of file
diff --git a/include/geos/noding/GeometryNoder.h b/include/geos/noding/GeometryNoder.h
index ea3b3c347..e31fd08cf 100644
--- a/include/geos/noding/GeometryNoder.h
+++ b/include/geos/noding/GeometryNoder.h
@@ -29,7 +29,7 @@ namespace geom {
 class Geometry;
 }
 namespace noding {
-class Noder;
+class ArcNoder;
 }
 }
 
@@ -43,6 +43,8 @@ public:
 
     GeometryNoder(const geom::Geometry& g);
 
+    ~GeometryNoder();
+
     std::unique_ptr<geom::Geometry> getNoded();
 
     // Declare type as noncopyable
@@ -52,17 +54,18 @@ public:
 private:
 
     const geom::Geometry& argGeom;
+    const bool argGeomHasCurves;
 
     SegmentString::NonConstVect lineList;
 
-    static void extractSegmentStrings(const geom::Geometry& g,
-                                      SegmentString::NonConstVect& to);
+    static void extractPathStrings(const geom::Geometry& g,
+                                   std::vector<std::unique_ptr<PathString>>& to);
 
-    Noder& getNoder();
+    ArcNoder& getNoder();
 
-    std::unique_ptr<Noder> noder;
+    std::unique_ptr<ArcNoder> noder;
 
-    std::unique_ptr<geom::Geometry> toGeometry(std::vector<std::unique_ptr<SegmentString>>& noded);
+    std::unique_ptr<geom::Geometry> toGeometry(std::vector<std::unique_ptr<PathString>>& noded) const;
 
 };
 
diff --git a/include/geos/noding/IteratedNoder.h b/include/geos/noding/IteratedNoder.h
index 1d2bc296a..2af2916bc 100644
--- a/include/geos/noding/IteratedNoder.h
+++ b/include/geos/noding/IteratedNoder.h
@@ -20,12 +20,13 @@
 
 #include <geos/export.h>
 
+#include <functional>
+#include <memory>
 #include <vector>
-#include <iostream>
 
 #include <geos/algorithm/LineIntersector.h>
 #include <geos/noding/SegmentString.h> // due to inlines
-#include <geos/noding/Noder.h> // for inheritance
+#include <geos/noding/ArcNoder.h> // for inheritance
 
 // Forward declarations
 namespace geos {
@@ -49,36 +50,36 @@ namespace noding { // geos::noding
  * Clients can choose to rerun the noding using a lower precision model.
  *
  */
-class GEOS_DLL IteratedNoder : public Noder { // implements Noder
+class GEOS_DLL IteratedNoder : public ArcNoder { // implements Noder
 
 private:
-    static const int MAX_ITER = 5;
+    static constexpr int MAX_ITER = 5;
 
 
     const geom::PrecisionModel* pm;
     algorithm::LineIntersector li;
-    std::vector<std::unique_ptr<SegmentString>> nodedSegStrings;
+    std::vector<std::unique_ptr<PathString>> nodedPaths;
     int maxIter;
+    std::function<std::unique_ptr<Noder>()> m_noderFunction;
 
     /**
      * Node the input segment strings once
      * and create the split edges between the nodes
      */
-    void node(const std::vector<SegmentString*>& segStrings,
+    void node(const std::vector<PathString*>& segStrings,
               int& numInteriorIntersections,
               geom::CoordinateXY& intersectionPoint);
 
+    static std::unique_ptr<Noder> createDefaultNoder();
+
 public:
 
-    IteratedNoder(const geom::PrecisionModel* newPm)
-        :
-        pm(newPm),
-        li(pm),
-        maxIter(MAX_ITER)
-    {
-    }
+    /** \brief
+     * Construct an IteratedNoder using a specific precisionModel and underlying Noder.
+     */
+    IteratedNoder(const geom::PrecisionModel* newPm, std::function<std::unique_ptr<Noder>()> noderFunction = createDefaultNoder);
 
-    ~IteratedNoder() override {}
+    ~IteratedNoder() override;
 
     /** \brief
      * Sets the maximum number of noding iterations performed before
@@ -96,23 +97,21 @@ public:
         maxIter = n;
     }
 
-    std::vector<std::unique_ptr<SegmentString>>
-    getNodedSubstrings() override
+    std::vector<std::unique_ptr<PathString>> getNodedPaths() override
     {
-        return std::move(nodedSegStrings);
+        return std::move(nodedPaths);
     }
 
-
     /** \brief
-     * Fully nodes a list of {@link SegmentString}s, i.e. performs noding iteratively
+     * Fully nodes a list of {@link PathString}s, i.e. performs noding iteratively
      * until no intersections are found between segments.
      *
      * Maintains labelling of edges correctly through the noding.
      *
-     * @param inputSegmentStrings a collection of SegmentStrings to be noded
+     * @param inputPathStrings a collection of SegmentStrings to be noded
      * @throws TopologyException if the iterated noding fails to converge.
      */
-    void computeNodes(const std::vector<SegmentString*>& inputSegmentStrings) override; // throw(GEOSException);
+    void computePathNodes(const std::vector<PathString*>& inputPathStrings) override;
 
     // Declare type as noncopyable
     IteratedNoder(IteratedNoder const&) = delete;
diff --git a/include/geos/noding/NodableArcString.h b/include/geos/noding/NodableArcString.h
index e1b612261..207f497b3 100644
--- a/include/geos/noding/NodableArcString.h
+++ b/include/geos/noding/NodableArcString.h
@@ -23,82 +23,22 @@ namespace geos::noding {
 class GEOS_DLL NodableArcString : public ArcString, public NodablePath {
 
 public:
-    using ArcString::ArcString;
+    //using ArcString::ArcString;
+
+    NodableArcString(std::vector<geom::CircularArc> arcs, std::unique_ptr<geom::CoordinateSequence> coords, bool constructZ, bool constructM, void* context);
+
+    std::unique_ptr<ArcString> clone() const;
 
     void addIntersection(geom::CoordinateXYZM intPt, size_t segmentIndex) override {
         m_adds[segmentIndex].push_back(intPt);
     }
 
-    static double pseudoAngleDiffCCW(double paStart, double pa) {
-        double diff = pa - paStart;
-
-        if (diff < 0) {
-            diff += 4;
-        }
-
-        return diff;
-    }
-
-    std::unique_ptr<ArcString> getNoded() {
-        if (m_adds.empty()) {
-            // use std::move ?
-            return std::make_unique<ArcString>(*this);
-        }
-
-        std::vector<geom::CircularArc> arcs;
-        for (size_t i = 0; i < m_arcs.size(); i++) {
-            auto it = m_adds.find(i);
-            if (it == m_adds.end()) {
-                arcs.push_back(m_arcs[i]);
-            } else {
-                std::vector<geom::CoordinateXYZM>& splitPoints = it->second;
-
-                // TODO check split point actually inside arc
-                // TODO ignore duplicate splitpoints
-
-                geom::CircularArc remainder = m_arcs[i];
-                const bool isCCW = remainder.getOrientation() == algorithm::Orientation::COUNTERCLOCKWISE;
-                const double paStart = geom::Quadrant::pseudoAngle(remainder.getCenter(), remainder.p0);
-                //double paStart = isCCW ? geom::Quadrant::pseudoAngle(remainder.getCenter(), remainder.p0) :
-                //                               geom::Quadrant::pseudoAngle(remainder.getCenter(), remainder.p2);
-
-                // Need a function for lengthFraction (pt);
-                std::sort(splitPoints.begin(), splitPoints.end(), [&remainder, paStart, isCCW](const auto& p0, const auto& p1) {
-                    double pa0 = geom::Quadrant::pseudoAngle(remainder.getCenter(), p0);
-                    double pa1 = geom::Quadrant::pseudoAngle(remainder.getCenter(), p1);
-
-                    // FIXME check this comparator...
-                    if (isCCW) {
-                        return pseudoAngleDiffCCW(paStart, pa0) < pseudoAngleDiffCCW(paStart, pa1);
-                    } else {
-                        return pseudoAngleDiffCCW(paStart, pa0) > pseudoAngleDiffCCW(paStart, pa1);
-                    }
-                });
-
-                std::cout << "arc " << remainder.toString() << " " << (isCCW ? "CCW" : "CW") << std::endl;
-                std::cout << "  paStart " << paStart << std::endl;
-                for (const auto& pt : splitPoints) {
-                    const double pa = geom::Quadrant::pseudoAngle(remainder.getCenter(), pt);
-                    std::cout << "  " << pt << " pa " << pa << " diff " << pseudoAngleDiffCCW(paStart, pa) << std::endl;
-                }
-
-                for (const auto& splitPoint : splitPoints) {
-                    if (!arcs.empty() && splitPoint.equals2D(arcs.back().p2)) {
-                        continue;
-                    }
-                    auto [a, b] = remainder.splitAtPoint(splitPoint);
-                    arcs.push_back(a);
-                    remainder = b;
-                }
-                arcs.push_back(remainder);
-            }
-        }
-
-        return std::make_unique<ArcString>(std::move(arcs));
-    }
+    std::unique_ptr<ArcString> getNoded();
 
 private:
     std::map<size_t, std::vector<geom::CoordinateXYZM>> m_adds;
+    bool m_constructZ = false;
+    bool m_constructM = false;
 };
 
 }
\ No newline at end of file
diff --git a/include/geos/noding/PathString.h b/include/geos/noding/PathString.h
index e1a3ba4b3..bc1a66990 100644
--- a/include/geos/noding/PathString.h
+++ b/include/geos/noding/PathString.h
@@ -17,6 +17,8 @@
 #include <geos/export.h>
 
 #include <cstddef>
+#include <memory>
+#include <vector>
 
 namespace geos::noding {
 
@@ -30,6 +32,9 @@ public:
     virtual std::size_t getSize() const = 0;
 
     virtual double getLength() const = 0;
+
+    std::vector<PathString*>
+    static toRawPointerVector(const std::vector<std::unique_ptr<PathString>> & segStrings);
 };
 
 }
diff --git a/include/geos/noding/SegmentString.h b/include/geos/noding/SegmentString.h
index d1aa26834..6648386df 100644
--- a/include/geos/noding/SegmentString.h
+++ b/include/geos/noding/SegmentString.h
@@ -181,6 +181,8 @@ public:
 
     static std::vector<SegmentString*> toRawPointerVector(const std::vector<std::unique_ptr<SegmentString>> & segStrings);
 
+    static std::vector<SegmentString*> toRawPointerVector(const std::vector<std::unique_ptr<PathString>> & segStrings);
+
     virtual std::ostream& print(std::ostream& os) const;
 
 protected:
diff --git a/include/geos/noding/SimpleNoder.h b/include/geos/noding/SimpleNoder.h
index 3be9796ec..3eb348e05 100644
--- a/include/geos/noding/SimpleNoder.h
+++ b/include/geos/noding/SimpleNoder.h
@@ -50,7 +50,7 @@ private:
 
 public:
 
-    SimpleNoder(std::unique_ptr<ArcIntersector>(nSegInt)) : ArcNoder(std::move(nSegInt)) {}
+    using ArcNoder::ArcNoder;
 
     void computePathNodes(const std::vector<PathString*>& inputSegmentStrings) override;
 
diff --git a/src/algorithm/Area.cpp b/src/algorithm/Area.cpp
index 2be5b5316..94889933d 100644
--- a/src/algorithm/Area.cpp
+++ b/src/algorithm/Area.cpp
@@ -136,7 +136,7 @@ Area::ofClosedCurve(const geom::Curve& ring) {
                 double triangleArea = 0.5*(p0.x*p2.y - p2.x*p0.y);
                 sum += triangleArea;
 
-                geom::CircularArc arc(p0, p1, p2);
+                geom::CircularArc arc(coords, j-2);
                 if (arc.isLinear()) {
                     continue;
                 }
diff --git a/src/algorithm/CircularArcIntersector.cpp b/src/algorithm/CircularArcIntersector.cpp
index 2ebbd247d..8f0c5dacb 100644
--- a/src/algorithm/CircularArcIntersector.cpp
+++ b/src/algorithm/CircularArcIntersector.cpp
@@ -3,7 +3,7 @@
  * GEOS - Geometry Engine Open Source
  * http://geos.osgeo.org
  *
- * Copyright (C) 2024 ISciences, LLC
+ * Copyright (C) 2024-2025 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
@@ -15,6 +15,13 @@
 #include <geos/algorithm/Angle.h>
 #include <geos/algorithm/CircularArcIntersector.h>
 #include <geos/algorithm/LineIntersector.h>
+#include <geos/geom/CoordinateSequences.h>
+
+#include <iomanip>
+
+using geos::geom::CoordinateSequence;
+using geos::geom::CoordinateXY;
+using geos::geom::CircularArc;
 
 namespace geos::algorithm {
 
@@ -29,8 +36,187 @@ nextAngleCCW(double from, double a, double b)
     }
 }
 
+static double
+angleFractionCCW(double x, double a, double b)
+{
+    if (x < a) {
+        x += 2*MATH_PI;
+    }
+    if (b < a) {
+        b += 2*MATH_PI;
+    }
+    return (x - a) / (b - a);
+}
+
+static double
+interpolateValue(double a1, double a2, double frac)
+{
+    frac = std::clamp(frac, 0.0, 1.0);
+    if (std::isnan(a1)) {
+        return a2;
+    }
+    if (std::isnan(a2)) {
+        return a1;
+    }
+    return a1 + frac * (a2 - a1);
+}
+
+static void interpolateZM(const CircularArc& arc,
+                   const CoordinateXY& pt,
+                   double& z, double& m)
+{
+    using geos::geom::Ordinate;
+
+    const geom::CoordinateSequence& seq = *arc.getCoordinateSequence();
+    std::size_t i0 = arc.getCoordinatePosition();
+
+    if (arc.p1().equals2D(pt)) {
+        seq.applyAt(i0 + 1, [&z, &m](const auto& arcPt) {
+            z = arcPt.template get<Ordinate::Z>();
+            m = arcPt.template get<Ordinate::M>();
+        });
+        return;
+    }
+
+    double z0, m0;
+    seq.applyAt(i0, [&z0, &m0](const auto& arcPt) {
+        z0 = arcPt.template get<Ordinate::Z>();
+        m0 = arcPt.template get<Ordinate::M>();
+    });
+    if (arc.p0().equals2D(pt)) {
+        z = z0;
+        m = m0;
+        return;
+    }
+
+    double z1, m1;
+    seq.applyAt(i0 + 1, [&z1, &m1](const auto& arcPt) {
+        z1 = arcPt.template get<Ordinate::Z>();
+        m1 = arcPt.template get<Ordinate::M>();
+    });
+
+    if (arc.p1().equals2D(pt)) {
+        z = z1;
+        m = m1;
+        return;
+    }
+
+
+    double z2, m2;
+    seq.applyAt(i0 + 2, [&z2, &m2](const auto& arcPt) {
+        z2 = arcPt.template get<Ordinate::Z>();
+        m2 = arcPt.template get<Ordinate::M>();
+    });
+
+    if (arc.p2().equals2D(pt)) {
+        z = z2;
+        m = m2;
+        return;
+    }
+
+    double theta0 = arc.theta0();
+    const double theta1 = arc.theta1();
+    double theta2 = arc.theta2();
+    const double theta = CircularArcs::getAngle(pt, arc.getCenter());
+
+    if (!arc.isCCW()) {
+        std::swap(theta0, theta2);
+        std::swap(z0, z2);
+        std::swap(m0, m2);
+    }
+
+    if (std::isnan(z1)) {
+        // Interpolate between p0 /  p2
+        const double frac = angleFractionCCW(theta, theta0, theta2);
+        z = interpolateValue(z0, z2, frac);
+    } else if (Angle::isWithinCCW(theta, theta0, theta1)) {
+        // Interpolate between p0 / p1
+        const double frac = angleFractionCCW(theta, theta0, theta1);
+        z = interpolateValue(z0, z1, frac);
+    } else {
+        // Interpolate between p1 / p2
+        const double frac = angleFractionCCW(theta, theta1, theta2);
+        z = interpolateValue(z1, z2, frac);
+    }
+
+    if (std::isnan(m1)) {
+        // Interpolate between p0 /  p2
+        const double frac = angleFractionCCW(theta, theta0, theta2);
+        m = interpolateValue(m0, m2, frac);
+    } else if (Angle::isWithinCCW(theta, theta0, theta1)) {
+        // Interpolate between p0 / p1
+        const double frac = angleFractionCCW(theta, theta0, theta1);
+        m = interpolateValue(m0, m1, frac);
+    } else {
+        // Interpolate between p1 / p2
+        const double frac = angleFractionCCW(theta, theta1, theta2);
+        m = interpolateValue(m1, m2, frac);
+    }
+
+}
+
+static void interpolateSegmentZM(const CoordinateSequence& seq,
+                              std::size_t ind0, std::size_t ind1,
+                              geom::CoordinateXY& pt, double& z, double& m)
+{
+    seq.applyAt(ind0, [&seq, &pt, ind1, &z, &m](const auto& p0) {
+        using CoordinateType = std::decay_t<decltype(p0)>;
+
+        const auto& p1 = seq.getAt<CoordinateType>(ind1);
+        z = Interpolate::zGetOrInterpolate(pt, p0, p1);
+        m = Interpolate::mGetOrInterpolate(pt, p0, p1);
+    });
+}
+
+
+static void interpolateZM(const CircularArc& arc0, const CircularArc& arc1, geom::CoordinateXYZM& pt)
+{
+    if (!std::isnan(pt.z) && !std::isnan(pt.m)) {
+        return;
+    }
+
+    double z0, m0;
+    double z1, m1;
+    interpolateZM(arc0, pt, z0, m0);
+    interpolateZM(arc1, pt, z1, m1);
+
+    if (std::isnan(pt.z)) {
+        pt.z = Interpolate::getOrAverage(z0, z1);
+    }
+    if (std::isnan(pt.m)) {
+        pt.m = Interpolate::getOrAverage(m0, m1);
+    }
+}
+
+static void interpolateZM(const CircularArc& arc0,
+                       const geom::CoordinateSequence& seq,
+                       std::size_t ind0, std::size_t ind1,
+                       geom::CoordinateXYZM& pt)
+{
+    if (!std::isnan(pt.z) && !std::isnan(pt.m)) {
+        return;
+    }
+
+    double z0, m0;
+    double z1, m1;
+    interpolateZM(arc0, pt, z0, m0);
+    interpolateSegmentZM(seq, ind0, ind1, pt, z1, m1);
+
+    if (std::isnan(pt.z)) {
+        pt.z = Interpolate::getOrAverage(z0, z1);
+    }
+    if (std::isnan(pt.m)) {
+        pt.m = Interpolate::getOrAverage(m0, m1);
+    }
+}
+
 int
-CircularArcIntersector::circleIntersects(const CoordinateXY& center, double r, const CoordinateXY& p0, const CoordinateXY& p1, CoordinateXY& ret0, CoordinateXY& ret1)
+CircularArcIntersector::circleIntersects(const CoordinateXY& center,
+                                         double r,
+                                         const CoordinateXY& p0,
+                                         const CoordinateXY& p1,
+                                         CoordinateXY& ret0,
+                                         CoordinateXY& ret1)
 {
     const double& x0 = center.x;
     const double& y0 = center.y;
@@ -92,10 +278,12 @@ CircularArcIntersector::circleIntersects(const CoordinateXY& center, double r, c
 }
 
 void
-CircularArcIntersector::intersects(const CircularArc& arc, const CoordinateXY& p0, const CoordinateXY& p1)
+CircularArcIntersector::intersects(const CircularArc& arc, const CoordinateSequence& seq, std::size_t segPos0, std::size_t segPos1, bool useSegEndpoints)
 {
     if (arc.isLinear()) {
-        intersects(arc.p0, arc.p2, p0, p1);
+        std::size_t arcPos0 = arc.getCoordinatePosition();
+        intersects(*arc.getCoordinateSequence(), arcPos0, arcPos0 + 2,
+                   seq, segPos0, segPos1);
         return;
     }
 
@@ -105,15 +293,15 @@ CircularArcIntersector::intersects(const CircularArc& arc, const CoordinateXY& p
     const CoordinateXY& c = arc.getCenter();
     const double r = arc.getRadius();
 
-    CoordinateXY isect0, isect1;
-    auto n = circleIntersects(c, r, p0, p1, isect0, isect1);
+    CoordinateXYZM isect0, isect1;
+    auto n = circleIntersects(c, r, seq.getAt<CoordinateXY>(segPos0), seq.getAt<CoordinateXY>(segPos1), isect0, isect1);
 
     if (n > 0 && arc.containsPointOnCircle(isect0)) {
-        intPt[nPt++] = isect0;
+        addIntersection(isect0, arc, seq, segPos0, segPos1, useSegEndpoints);
     }
 
     if (n > 1  && arc.containsPointOnCircle(isect1)) {
-        intPt[nPt++] = isect1;
+        addIntersection(isect1, arc, seq, segPos0, segPos1, useSegEndpoints);
     }
 
     switch (nPt) {
@@ -128,42 +316,24 @@ CircularArcIntersector::intersects(const CircularArc& arc, const CoordinateXY& p
     }
 }
 
-void
-CircularArcIntersector::intersects(const CoordinateXY& p0, const CoordinateXY& p1,
-                                   const CoordinateXY& q0, const CoordinateXY& q1)
-{
-    reset();
-
-    algorithm::LineIntersector li;
-    li.computeIntersection(p0, p1, q0, q1);
-    if (li.getIntersectionNum() == 2) {
-        // FIXME this means a collinear intersection, so we should report as cocircular?
-        intPt[0] = li.getIntersection(0);
-        intPt[1] = li.getIntersection(1);
-        result = TWO_POINT_INTERSECTION;
-    } else if (li.getIntersectionNum() == 1) {
-        intPt[0] = li.getIntersection(0);
-        nPt = 1;
-        result = ONE_POINT_INTERSECTION;
-    } else {
-        result = NO_INTERSECTION;
-    }
-}
-
 void
 CircularArcIntersector::intersects(const CircularArc& arc1, const CircularArc& arc2)
 {
     // Handle cases where one or both arcs are degenerate
     if (arc1.isLinear()) {
         if (arc2.isLinear()) {
-            intersects(arc1.p0, arc1.p2, arc2.p0, arc2.p2);
+            const auto arc1pos = arc1.getCoordinatePosition();
+            const auto arc2pos = arc2.getCoordinatePosition();
+
+            intersects(*arc1.getCoordinateSequence(), arc1pos, arc1pos + 2,
+                       *arc2.getCoordinateSequence(), arc2pos, arc2pos + 2);
             return;
         } else {
-            intersects(arc2, arc1.p0, arc1.p2);
+            intersects(arc2, *arc1.getCoordinateSequence(), arc1.getCoordinatePosition(), arc1.getCoordinatePosition() + 2, true);
             return;
         }
     } else if (arc2.isLinear()) {
-        intersects(arc1, arc2.p0, arc2.p2);
+        intersects(arc1, *arc2.getCoordinateSequence(), arc2.getCoordinatePosition(), arc2.getCoordinatePosition() + 2, false);
         return;
     }
 
@@ -205,8 +375,10 @@ CircularArcIntersector::intersects(const CircularArc& arc1, const CircularArc& a
         double bp0 = arc2.theta0();
         double bp1 = arc2.theta2();
 
+        // Orientation of the result matches the first input
         bool resultArcIsCCW = true;
 
+        // Make both inputs counter-clockwise for the purpose of determining intersections
         if (arc1.getOrientation() != Orientation::COUNTERCLOCKWISE) {
             std::swap(ap0, ap1);
             resultArcIsCCW = false;
@@ -220,50 +392,70 @@ CircularArcIntersector::intersects(const CircularArc& arc1, const CircularArc& a
         bp1 = Angle::normalizePositive(bp1);
 
         bool checkBp1inA = true;
+        bool checkAcontained = true;
+
+        // Possible intersection arrangements:
+        // A contained within B
+        // A overlaps B
+        // B contained within A
 
         // check start of B within A?
         if (Angle::isWithinCCW(bp0, ap0, ap1)) {
-            double start = bp0;
-            double end = nextAngleCCW(start, bp1, ap1);
+            checkAcontained = false;
+            const double start = bp0;
+            const double end = nextAngleCCW(start, bp1, ap1);
 
             if (end == bp1) {
                 checkBp1inA = false;
             }
 
             if (start == end) {
-                intPt[nPt++] = CircularArcs::createPoint(c1, r1, start);
+                const CoordinateXY computedIntPt = CircularArcs::createPoint(c1, r1, start);
+                addIntersection(computedIntPt, arc1, arc2);
             }
             else {
                 if (resultArcIsCCW) {
-                    intArc[nArc++] = CircularArc(start, end, c1, r1, Orientation::COUNTERCLOCKWISE);
+                    addArcIntersection(start, end, Orientation::COUNTERCLOCKWISE, arc1, arc2);
                 }
                 else {
-                    intArc[nArc++] = CircularArc(end, start, c1, r1, Orientation::CLOCKWISE);
+                    addArcIntersection(end, start, Orientation::CLOCKWISE, arc1, arc2);
                 }
             }
         }
 
         if (checkBp1inA && Angle::isWithinCCW(bp1, ap0, ap1)) {
             // end of B within A?
-            double start = ap0;
-            double end = bp1;
+            checkAcontained = false;
+
+            const double start = ap0;
+            const double end = bp1;
             if (start == end) {
-                intPt[nPt++] = CircularArcs::createPoint(c1, r1, start);
+                const CoordinateXY computedIntPt = CircularArcs::createPoint(c1, r1, start);
+                addIntersection(computedIntPt, arc1, arc2);
             }
             else {
                 if (resultArcIsCCW) {
-                    intArc[nArc++] = CircularArc(start, end, c1, r1, Orientation::COUNTERCLOCKWISE);
+                    addArcIntersection(start, end, Orientation::CLOCKWISE, arc1, arc2);
                 }
                 else {
-                    intArc[nArc++] = CircularArc(end, start, c1, r1, Orientation::CLOCKWISE);
+                    addArcIntersection(end, start, Orientation::CLOCKWISE, arc1, arc2);
                 }
             }
         }
+
+        if (checkAcontained && Angle::isWithinCCW(ap0, bp0 , bp1) && ap0 != bp0  && ap0 != bp1 && Angle::isWithinCCW(ap1, bp0, bp1) && ap1 != bp1 && ap1 != bp0) {
+            if (resultArcIsCCW) {
+                addArcIntersection(ap0, ap1, Orientation::COUNTERCLOCKWISE, arc1, arc2);
+            }
+            else {
+                addArcIntersection(ap1, ap0, Orientation::CLOCKWISE, arc1, arc2);
+            }
+        }
     } else {
         // NOT COCIRCULAR
 
-        double dx = c2.x-c1.x;
-        double dy = c2.y-c1.y;
+        const double dx = c2.x-c1.x;
+        const double dy = c2.y-c1.y;
 
 #if 1
         // point where a line between the two circle center points intersects
@@ -271,16 +463,19 @@ CircularArcIntersector::intersects(const CircularArc& arc1, const CircularArc& a
         CoordinateXY p{c1.x + a* dx/d, c1.y+a* dy/d};
 
         // distance from p to the intersection points
-        double h = std::sqrt(r1*r1 - a*a);
+        const double h = std::sqrt(r1*r1 - a*a);
 
-        CoordinateXY isect0{p.x + h* dy/d, p.y - h* dx/d };
-        CoordinateXY isect1{p.x - h* dy/d, p.y + h* dx/d };
+        const CoordinateXY isect0{p.x + h* dy/d, p.y - h* dx/d };
+        const CoordinateXY isect1{p.x - h* dy/d, p.y + h* dx/d };
 
-        if (arc1.containsPointOnCircle(isect0) && arc2.containsPointOnCircle(isect0)) {
-            intPt[nPt++] = isect0;
-        }
-        if (!isect1.equals2D(isect0) && arc1.containsPointOnCircle(isect1) && arc2.containsPointOnCircle(isect1)) {
-            intPt[nPt++] = isect1;
+        for (const CoordinateXY& computedIntPt : {isect0, isect1}) {
+            if (nPt > 0 && computedIntPt.equals2D(intPt[0])) {
+                continue;
+            }
+
+            if (arc1.containsPointOnCircle(computedIntPt) && arc2.containsPointOnCircle(computedIntPt)) {
+                addIntersection(computedIntPt, arc1, arc2);
+            }
         }
 #else
         // Alternate formulation.
@@ -311,6 +506,25 @@ CircularArcIntersector::intersects(const CircularArc& arc1, const CircularArc& a
             }
         }
 #endif
+
+#if 1
+        // Add endpoint intersections missed due to precision issues.
+        // TODO: Add some logic to prevent double-counting of endpoints. Ideally, the endpoint test would happen before
+        // computing intersection points, so if there is an endpoint intersection we get the exact intersection point
+        // instead of a computed one.
+        if (nPt < 2 && arc1.p0().equals2D(arc2.p0()) && (nPt == 0 || !intPt[0].equals2D(arc1.p0()))) {
+            addIntersection(arc1.p0(), arc1, arc2);
+        }
+        if (nPt < 2 && arc1.p0().equals2D(arc2.p2()) && (nPt == 0 || !intPt[0].equals2D(arc1.p0()))) {
+            addIntersection(arc1.p0(), arc1, arc2);
+        }
+        if (nPt < 2 && arc1.p2().equals2D(arc2.p0()) && (nPt == 0 || !intPt[0].equals2D(arc1.p2()))) {
+            addIntersection(arc1.p2(), arc1, arc2);
+        }
+        if (nPt < 2 && arc1.p2().equals2D(arc2.p2()) && (nPt == 0 || !intPt[0].equals2D(arc1.p2()))) {
+            addIntersection(arc1.p2(), arc1, arc2);
+        }
+#endif
     }
 
     if (nArc) {
@@ -331,4 +545,156 @@ CircularArcIntersector::intersects(const CircularArc& arc1, const CircularArc& a
     }
 }
 
+void
+CircularArcIntersector::intersects(const CoordinateSequence &p, std::size_t p0, std::size_t p1,
+                                   const CoordinateSequence &q, std::size_t q0, std::size_t q1)
+{
+    LineIntersector li;
+    li.computeIntersection(p, p0, p1, q, q0, q1);
+
+    if (li.getIntersectionNum() == 2) {
+        // FIXME this means a collinear intersection, so we should report as cocircular?
+        intPt[0] = li.getIntersection(0);
+        intPt[1] = li.getIntersection(1);
+        result = TWO_POINT_INTERSECTION;
+    } else if (li.getIntersectionNum() == 1) {
+        intPt[0] = li.getIntersection(0);
+        nPt = 1;
+        result = ONE_POINT_INTERSECTION;
+    } else {
+        result = NO_INTERSECTION;
+    }
 }
+
+void
+CircularArcIntersector::addArcIntersection(double startAngle, double endAngle, int orientation, const CircularArc& arc1, const CircularArc& arc2)
+{
+    const auto theta1  = CircularArcs::getMidpointAngle(startAngle, endAngle, orientation == Orientation::COUNTERCLOCKWISE);
+    const CoordinateXY& center = arc1.getCenter();
+    const double radius = arc1.getRadius();
+
+    const bool constructZ = arc1.getCoordinateSequence()->hasZ() || arc2.getCoordinateSequence()->hasZ();
+    const bool constructM = arc1.getCoordinateSequence()->hasM() || arc2.getCoordinateSequence()->hasM();
+
+    CoordinateXYZM computedStartPt(CircularArcs::createPoint(center, radius, startAngle));
+    CoordinateXYZM computedMidPt(CircularArcs::createPoint(center, radius, theta1));
+    CoordinateXYZM computedEndPt(CircularArcs::createPoint(center, radius, endAngle));
+
+    if (computedStartPt.equals2D(arc1.p0())) {
+        arc1.applyAt(0, [&computedStartPt](const auto& endpoint) {
+            computedStartPt.z = Interpolate::zGet(computedStartPt, endpoint);
+            computedStartPt.m = Interpolate::mGet(computedStartPt, endpoint);
+        });
+    } else if (computedStartPt.equals2D(arc1.p2())) {
+        arc1.applyAt(2, [&computedStartPt](const auto& endpoint) {
+            computedStartPt.z = Interpolate::zGet(computedStartPt, endpoint);
+            computedStartPt.m = Interpolate::mGet(computedStartPt, endpoint);
+        });
+    } else if (computedStartPt.equals2D(arc2.p0())) {
+        arc2.applyAt(0, [&computedStartPt](const auto& endpoint) {
+            computedStartPt.z = Interpolate::zGet(computedStartPt, endpoint);
+            computedStartPt.m = Interpolate::mGet(computedStartPt, endpoint);
+        });
+    } else if (computedStartPt.equals2D(arc2.p2())) {
+        arc2.applyAt(2, [&computedStartPt](const auto& endpoint) {
+            computedStartPt.z = Interpolate::zGet(computedStartPt, endpoint);
+            computedStartPt.m = Interpolate::mGet(computedStartPt, endpoint);
+        });
+    }
+
+    if (computedEndPt.equals2D(arc1.p0())) {
+        arc1.applyAt(0, [&computedEndPt](const auto& endpoint) {
+            computedEndPt.z = Interpolate::zGet(computedEndPt, endpoint);
+            computedEndPt.m = Interpolate::mGet(computedEndPt, endpoint);
+        });
+    } else if (computedEndPt.equals2D(arc1.p2())) {
+        arc1.applyAt(2, [&computedEndPt](const auto& endpoint) {
+            computedEndPt.z = Interpolate::zGet(computedEndPt, endpoint);
+            computedEndPt.m = Interpolate::mGet(computedEndPt, endpoint);
+        });
+    } else if (computedEndPt.equals2D(arc2.p0())) {
+        arc2.applyAt(0, [&computedEndPt](const auto& endpoint) {
+            computedEndPt.z = Interpolate::zGet(computedEndPt, endpoint);
+            computedEndPt.m = Interpolate::mGet(computedEndPt, endpoint);
+        });
+    } else if (computedEndPt.equals2D(arc2.p2())) {
+        arc2.applyAt(2, [&computedEndPt](const auto& endpoint) {
+            computedEndPt.z = Interpolate::zGet(computedEndPt, endpoint);
+            computedEndPt.m = Interpolate::mGet(computedEndPt, endpoint);
+        });
+    }
+
+    interpolateZM(arc1, arc2, computedStartPt);
+    interpolateZM(arc1, arc2, computedMidPt);
+    interpolateZM(arc1, arc2, computedEndPt);
+
+    auto seq = std::make_unique<CoordinateSequence>(3, constructZ, constructM);
+    seq->setAt(computedStartPt, 0);
+    seq->setAt(computedMidPt, 1);
+    seq->setAt(computedEndPt, 2);
+
+    intArc[nArc++] = CircularArc(std::move(seq), 0, center, radius, orientation);
+}
+
+void
+CircularArcIntersector::addIntersection(const CoordinateXY& computedIntPt, const CircularArc& arc1, const CircularArc& arc2) {
+    CoordinateXYZM& newIntPt = intPt[nPt++];
+    newIntPt = computedIntPt;
+
+    if (computedIntPt.equals2D(arc1.p0())) {
+        arc1.applyAt(0, [&newIntPt](const auto& endpoint) {
+            newIntPt.z = Interpolate::zGet(newIntPt, endpoint);
+            newIntPt.m = Interpolate::mGet(newIntPt, endpoint);
+        });
+    } else if (computedIntPt.equals2D(arc1.p2())) {
+        arc1.applyAt(2, [&newIntPt](const auto& endpoint) {
+            newIntPt.z = Interpolate::zGet(newIntPt, endpoint);
+            newIntPt.m = Interpolate::mGet(newIntPt, endpoint);
+        });
+    }
+
+    interpolateZM(arc1, arc2, newIntPt);
+}
+
+void
+CircularArcIntersector::addIntersection(const CoordinateXY& computedIntPt, const CircularArc& arc1, const CoordinateSequence& seq, std::size_t pos0, std::size_t pos1, bool useSegEndpoints) {
+    CoordinateXYZM& newIntPt = intPt[nPt++];
+    newIntPt = computedIntPt;
+
+    for (int i = 0; i < 2; i++) {
+        if (useSegEndpoints) {
+            if (computedIntPt.equals2D(seq.getAt<CoordinateXY>(pos0))) {
+                seq.applyAt(pos0, [&newIntPt](const auto& endpoint) {
+                    newIntPt.z = Interpolate::zGet(newIntPt, endpoint);
+                    newIntPt.m = Interpolate::mGet(newIntPt, endpoint);
+                });
+            }
+            if (computedIntPt.equals2D(seq.getAt<CoordinateXY>(pos1))) {
+                seq.applyAt(pos1, [&newIntPt](const auto& endpoint) {
+                    newIntPt.z = Interpolate::zGet(newIntPt, endpoint);
+                    newIntPt.m = Interpolate::mGet(newIntPt, endpoint);
+                });
+            }
+        } else {
+            if (computedIntPt.equals2D(arc1.p0())) {
+                arc1.applyAt(0, [&newIntPt](const auto& endpoint) {
+                    newIntPt.z = Interpolate::zGet(newIntPt, endpoint);
+                    newIntPt.m = Interpolate::mGet(newIntPt, endpoint);
+                });
+            }
+            if (computedIntPt.equals2D(arc1.p2())) {
+                arc1.applyAt(2, [&newIntPt](const auto& endpoint) {
+                    newIntPt.z = Interpolate::zGet(newIntPt, endpoint);
+                    newIntPt.m = Interpolate::mGet(newIntPt, endpoint);
+                });
+            }
+        }
+
+        useSegEndpoints = !useSegEndpoints;
+    }
+
+    interpolateZM(arc1, seq, pos0, pos1, newIntPt);
+}
+
+
+}
\ No newline at end of file
diff --git a/src/algorithm/LineIntersector.cpp b/src/algorithm/LineIntersector.cpp
index b3901b9b3..f4ce40836 100644
--- a/src/algorithm/LineIntersector.cpp
+++ b/src/algorithm/LineIntersector.cpp
@@ -181,16 +181,35 @@ public:
                 std::size_t i1) :
         m_li(li),
         m_seq0(seq0),
-        m_i0(i0),
+        m_i00(i0),
+        m_i01(i0 + 1),
         m_seq1(seq1),
-        m_i1(i1) {}
+        m_i10(i1),
+        m_i11(i1 + 1)
+    {}
+
+    DoIntersect(algorithm::LineIntersector& li,
+                const CoordinateSequence& seq0,
+                std::size_t i00,
+                std::size_t i01,
+                const CoordinateSequence& seq1,
+                std::size_t i10,
+                std::size_t i11) :
+        m_li(li),
+        m_seq0(seq0),
+        m_i00(i00),
+        m_i01(i01),
+        m_seq1(seq1),
+        m_i10(i10),
+        m_i11(i11)
+    {}
 
     template<typename T1, typename T2>
     void operator()() {
-        const T1& p00 = m_seq0.getAt<T1>(m_i0);
-        const T1& p01 = m_seq0.getAt<T1>(m_i0 + 1);
-        const T2& p10 = m_seq1.getAt<T2>(m_i1);
-        const T2& p11 = m_seq1.getAt<T2>(m_i1 + 1);
+        const T1& p00 = m_seq0.getAt<T1>(m_i00);
+        const T1& p01 = m_seq0.getAt<T1>(m_i01);
+        const T2& p10 = m_seq1.getAt<T2>(m_i10);
+        const T2& p11 = m_seq1.getAt<T2>(m_i11);
 
         m_li.computeIntersection(p00, p01, p10, p11);
     }
@@ -198,9 +217,11 @@ public:
 private:
     algorithm::LineIntersector& m_li;
     const CoordinateSequence& m_seq0;
-    std::size_t m_i0;
+    std::size_t m_i00;
+    std::size_t m_i01;
     const CoordinateSequence& m_seq1;
-    std::size_t m_i1;
+    std::size_t m_i10;
+    std::size_t m_i11;
 };
 
 /*public*/
@@ -212,6 +233,15 @@ LineIntersector::computeIntersection(const CoordinateSequence& p, std::size_t p0
     CoordinateSequences::binaryDispatch(p, q, dis);
 }
 
+/*public*/
+void
+LineIntersector::computeIntersection(const CoordinateSequence& p, std::size_t p0, std::size_t p1,
+                                     const CoordinateSequence& q, std::size_t q0, std::size_t q1)
+{
+    DoIntersect dis(*this, p, p0, p1 ,q, q0, q1);
+    CoordinateSequences::binaryDispatch(p, q, dis);
+}
+
 /* private static */
 const CoordinateXY&
 LineIntersector::nearestEndpoint(const CoordinateXY& p1, const CoordinateXY& p2,
diff --git a/src/algorithm/RayCrossingCounter.cpp b/src/algorithm/RayCrossingCounter.cpp
index 9e25deba8..3d212beb6 100644
--- a/src/algorithm/RayCrossingCounter.cpp
+++ b/src/algorithm/RayCrossingCounter.cpp
@@ -113,11 +113,8 @@ RayCrossingCounter::processSequence(const geom::CoordinateSequence& seq, bool is
         }
     } else {
         for (std::size_t i = 2; i < seq.size(); i += 2) {
-            const geom::CoordinateXY& p1 = seq.getAt<geom::CoordinateXY>(i-2);
-            const geom::CoordinateXY& p2 = seq.getAt<geom::CoordinateXY>(i-1);
-            const geom::CoordinateXY& p3 = seq.getAt<geom::CoordinateXY>(i);
-
-            countArc(p1, p2, p3);
+            geom::CircularArc arc(seq, i-2);
+            countArc(arc);
 
             if (isOnSegment()) 	{
                 return;
@@ -197,9 +194,9 @@ RayCrossingCounter::shouldCountCrossing(const geom::CircularArc& arc, const geom
     // a) is in the interior of the arc
     // b) is at the starting point of the arc, and the arc is directed upward at that point
     // c) is at the ending point of the arc is directed downward at that point
-    if (q.equals2D(arc.p0)) {
+    if (q.equals2D(arc.p0())) {
         return arc.isUpwardAtPoint(q);
-    } else if (q.equals2D(arc.p2)) {
+    } else if (q.equals2D(arc.p2())) {
         return !arc.isUpwardAtPoint(q);
     } else {
         return true;
@@ -252,25 +249,22 @@ RayCrossingCounter::pointsIntersectingHorizontalRay(const geom::CircularArc& arc
 }
 
 void
-RayCrossingCounter::countArc(const CoordinateXY& p1,
-                             const CoordinateXY& p2,
-                             const CoordinateXY& p3)
+RayCrossingCounter::countArc(const geom::CircularArc& arc)
 {
     // For each arc, check if it crosses
     // a horizontal ray running from the test point in
     // the positive x direction.
-    geom::CircularArc arc(p1, p2, p3);
 
     // If the arc is degenerate, process it is two line segments
     if (arc.isLinear()) {
-        countSegment(p1, p2);
-        countSegment(p2, p3);
+        countSegment(arc.p0(), arc.p1());
+        countSegment(arc.p1(), arc.p2());
         return;
     }
 
     // Check if the arc is strictly to the left of the test point
     geom::Envelope arcEnvelope;
-    CircularArcs::expandEnvelope(arcEnvelope, p1, p2, p3);
+    CircularArcs::expandEnvelope(arcEnvelope, arc.p0(), arc.p1(), arc.p2());
 
     if (arcEnvelope.getMaxX() < point.x) {
         return;
diff --git a/src/geom/CircularArc.cpp b/src/geom/CircularArc.cpp
new file mode 100644
index 000000000..eb2ffe5df
--- /dev/null
+++ b/src/geom/CircularArc.cpp
@@ -0,0 +1,416 @@
+/**********************************************************************
+*
+ * GEOS - Geometry Engine Open Source
+ * http://geos.osgeo.org
+ *
+ * Copyright (C) 2024-2025 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/CircularArc.h>
+#include <geos/triangulate/quadedge/TrianglePredicate.h>
+#include <sstream>
+
+namespace geos::geom {
+
+CircularArc::CircularArc() :
+    m_seq(nullptr),
+    m_center_known(true),
+    m_radius_known(true),
+    m_orientation_known(true),
+    m_own_coordinates(true)
+{}
+
+template<typename CoordType>
+static CircularArc createFromPoints(const CoordType& p0, const CoordType& p2, const CoordinateXY& center, double radius, int orientation)
+{
+    static_assert(std::is_base_of_v<CoordinateXY, CoordType>);
+
+    constexpr bool hasZ = CoordType::template has<Ordinate::Z>();
+    constexpr bool hasM = CoordType::template has<Ordinate::M>();
+
+    auto seq = std::make_unique<CoordinateSequence>(3, hasZ, hasM);
+
+    CoordType p1(geos::algorithm::CircularArcs::getMidpoint(p0, p2, center, radius, orientation == algorithm::Orientation::COUNTERCLOCKWISE));
+    if constexpr (hasZ) {
+        p1.z = 0.5*(p0.z + p2.z);
+    }
+    if constexpr (hasM) {
+        p1.m = 0.5*(p0.m + p2.m);
+    }
+
+    seq->setAt(p0, 0);
+    seq->setAt(p1, 1);
+    seq->setAt(p2, 2);
+
+    CircularArc ret(std::move(seq), 0);
+
+    return ret;
+}
+
+CircularArc
+CircularArc::create(const CoordinateXY& p0, const CoordinateXY& p2, const CoordinateXY& center, double radius, int orientation) {
+    return createFromPoints(p0, p2, center, radius, orientation);
+}
+
+CircularArc
+CircularArc::create(const Coordinate& p0, const Coordinate& p2, const CoordinateXY& center, double radius, int orientation) {
+    return createFromPoints(p0, p2, center, radius, orientation);
+}
+
+CircularArc
+CircularArc::create(const CoordinateXYM& p0, const CoordinateXYM& p2, const CoordinateXY& center, double radius, int orientation) {
+    return createFromPoints(p0, p2, center, radius, orientation);
+}
+
+CircularArc
+CircularArc::create(const CoordinateXYZM& p0, const CoordinateXYZM& p2, const CoordinateXY& center, double radius, int orientation) {
+    return createFromPoints(p0, p2, center, radius, orientation);
+}
+
+CircularArc::CircularArc(const CoordinateSequence& seq, std::size_t pos) :
+    m_seq(&seq),
+    m_pos(pos),
+    m_own_coordinates(false) {}
+
+CircularArc::CircularArc(std::unique_ptr<CoordinateSequence> seq, std::size_t pos) :
+    CircularArc(*seq, pos)
+{
+    m_own_coordinates = true;
+    seq.release();
+}
+
+CircularArc::CircularArc(const CoordinateSequence& seq, std::size_t pos, const CoordinateXY& center, double radius, int orientation) :
+    m_seq(&seq),
+    m_pos(pos),
+    m_center(center),
+    m_radius(radius),
+    m_orientation(orientation),
+    m_center_known(true),
+    m_radius_known(true),
+    m_orientation_known(true),
+    m_own_coordinates(false)
+{}
+
+CircularArc::CircularArc(std::unique_ptr<CoordinateSequence> seq, std::size_t pos, const CoordinateXY& center, double radius, int orientation) :
+    CircularArc(*seq, pos, center, radius, orientation)
+{
+    m_own_coordinates = true;
+    seq.release();
+}
+
+CircularArc::CircularArc(const CircularArc& other) :
+    m_seq(new CoordinateSequence(0, other.getCoordinateSequence()->hasZ(), other.getCoordinateSequence()->hasM())),
+    m_pos(0),
+    m_center(other.m_center),
+    m_radius(other.m_radius),
+    m_orientation(other.m_orientation),
+    m_center_known(other.m_center_known),
+    m_radius_known(other.m_radius_known),
+    m_orientation_known(other.m_orientation_known),
+    m_own_coordinates(true)
+{
+    CoordinateSequence* seq = const_cast<CoordinateSequence*>(m_seq);
+    seq->reserve(3);
+    seq->add(*other.getCoordinateSequence(), other.getCoordinatePosition(), other.getCoordinatePosition() + 2);
+}
+
+CircularArc::CircularArc(CircularArc&& other) noexcept {
+    m_seq = other.m_seq;
+    m_pos = other.m_pos;
+    m_center = other.m_center;
+    m_radius = other.m_radius;
+    m_orientation = other.m_orientation;
+    m_center_known = other.m_center_known;
+    m_radius_known = other.m_radius_known;
+    m_orientation_known = other.m_orientation_known;
+    m_own_coordinates = other.m_own_coordinates;
+
+    if (other.m_own_coordinates) {
+        other.m_own_coordinates = false;
+    }
+}
+
+CircularArc&
+CircularArc::operator=(const CircularArc& other)
+{
+    if (m_own_coordinates) {
+        delete m_seq;
+    }
+
+    m_seq = new CoordinateSequence(0, other.getCoordinateSequence()->hasZ(), other.getCoordinateSequence()->hasM());
+    m_pos = other.m_pos;
+    m_own_coordinates = true;
+    m_orientation = other.m_orientation;
+    m_orientation_known = other.m_orientation_known;
+    m_center = other.m_center;
+    m_center_known = other.m_center_known;
+    m_radius = other.m_radius;
+    m_radius_known = other.m_radius_known;
+
+    CoordinateSequence* seq = const_cast<CoordinateSequence*>(m_seq);
+    seq->reserve(3);
+    seq->add(*other.getCoordinateSequence(), other.getCoordinatePosition(), other.getCoordinatePosition() + 2);
+
+    return *this;
+}
+
+CircularArc&
+CircularArc::operator=(CircularArc&& other) noexcept
+{
+    if (m_own_coordinates) {
+        delete m_seq;
+    }
+
+    m_seq = other.m_seq;
+    m_pos = other.m_pos;
+    m_own_coordinates = other.m_own_coordinates;
+    m_orientation = other.m_orientation;
+    m_orientation_known = other.m_orientation_known;
+    m_center = other.m_center;
+    m_center_known = other.m_center_known;
+    m_radius = other.m_radius;
+    m_radius_known = other.m_radius_known;
+
+    if (m_own_coordinates) {
+        other.m_own_coordinates = false;
+    }
+
+    return *this;
+}
+
+CircularArc::~CircularArc()
+{
+    if (m_own_coordinates) {
+        delete m_seq;
+    }
+}
+
+bool
+CircularArc::containsAngle(double theta) const {
+    auto t0 = theta0();
+    auto t2 = theta2();
+
+    if (theta == t0 || theta == t2) {
+        return true;
+    }
+
+    if (getOrientation() == algorithm::Orientation::COUNTERCLOCKWISE) {
+        std::swap(t0, t2);
+    }
+
+    t2 -= t0;
+    theta -= t0;
+
+    if (t2 < 0) {
+        t2 += 2*MATH_PI;
+    }
+    if (theta < 0) {
+        theta += 2*MATH_PI;
+    }
+
+    return theta >= t2;
+}
+
+bool
+CircularArc::containsPoint(const CoordinateXY& q) const
+{
+    if (q == p0() || q == p1() || q == p2()) {
+        return true;
+    }
+
+    //auto dist = std::abs(q.distance(getCenter()) - getRadius());
+
+    //if (dist > 1e-8) {
+    //    return false;
+    //}
+
+    if (triangulate::quadedge::TrianglePredicate::isInCircleRobust(p0(), p1(), p2(), q) != geom::Location::BOUNDARY) {
+        return false;
+    }
+
+    return containsPointOnCircle(q);
+}
+
+double
+CircularArc::getAngle() const
+{
+    if (isCircle()) {
+        return 2*MATH_PI;
+    }
+
+    /// Even Rouault:
+    /// potential optimization?: using crossproduct(p0 - center, p2 - center) = radius * radius * sin(angle)
+    /// could yield the result by just doing a single asin(), instead of 2 atan2()
+    /// actually one should also likely compute dotproduct(p0 - center, p2 - center) = radius * radius * cos(angle),
+    /// and thus angle = atan2(crossproduct(p0 - center, p2 - center) , dotproduct(p0 - center, p2 - center) )
+    auto t0 = theta0();
+    auto t2 = theta2();
+
+    if (getOrientation() == algorithm::Orientation::COUNTERCLOCKWISE) {
+        std::swap(t0, t2);
+    }
+
+    if (t0 < t2) {
+        t0 += 2*MATH_PI;
+    }
+
+    auto diff = t0-t2;
+
+    return diff;
+}
+
+double
+CircularArc::getArea() const {
+    if (isLinear()) {
+        return 0;
+    }
+
+    auto R = getRadius();
+    auto theta = getAngle();
+    return R*R/2*(theta - std::sin(theta));
+}
+
+double
+CircularArc::getLength() const {
+    if (isLinear()) {
+        return p0().distance(p2());
+    }
+
+    return getAngle()*getRadius();
+}
+
+bool
+CircularArc::isUpwardAtPoint(const CoordinateXY& q) const {
+    auto quad = geom::Quadrant::quadrant(getCenter(), q);
+    bool isUpward;
+
+    if (getOrientation() == algorithm::Orientation::CLOCKWISE) {
+        isUpward = (quad == geom::Quadrant::SW || quad == geom::Quadrant::NW);
+    } else {
+        isUpward = (quad == geom::Quadrant::SE || quad == geom::Quadrant::NE);
+    }
+
+    return isUpward;
+}
+
+CircularArc
+CircularArc::reverse() const
+{
+    auto seq = std::make_unique<CoordinateSequence>(3, m_seq->hasZ(), m_seq->hasM());
+    m_seq->applyAt(m_pos, [&seq](const auto& pt) {
+        seq->setAt(pt, 2);
+    });
+    m_seq->applyAt(m_pos + 1, [&seq](const auto& pt) {
+        seq->setAt(pt, 1);
+    });
+    m_seq->applyAt(m_pos + 2, [&seq](const auto& pt) {
+        seq->setAt(pt, 0);
+    });
+
+    CircularArc ret(std::move(seq), 0);
+
+    if (m_orientation_known) {
+        if (m_orientation == algorithm::Orientation::COUNTERCLOCKWISE) {
+            ret.m_orientation = algorithm::Orientation::CLOCKWISE;
+        } else if (m_orientation == algorithm::Orientation::CLOCKWISE) {
+            ret.m_orientation = algorithm::Orientation::COUNTERCLOCKWISE;
+        } else {
+            ret.m_orientation = algorithm::Orientation::COLLINEAR;
+        }
+        ret.m_orientation_known = true;
+    }
+
+    if (m_center_known) {
+        ret.m_center = m_center;
+        ret.m_center_known = true;
+    }
+
+    if (m_radius_known) {
+        ret.m_radius = m_radius;
+        ret.m_radius_known = true;
+    }
+
+    return ret;
+}
+
+bool
+CircularArc::equals(const CircularArc &other, double tol) const
+{
+    if (getCoordinateSequence()->hasZ() != other.getCoordinateSequence()->hasZ()) {
+        return false;
+    }
+
+    if (getCoordinateSequence()->hasM() != other.getCoordinateSequence()->hasM()) {
+        return false;
+    }
+
+    if (getCenter().distance(other.getCenter()) > tol) {
+        return false;
+    }
+
+    if (std::abs(getRadius() - other.getRadius()) > tol) {
+        return false;
+    }
+
+    if (getOrientation() != other.getOrientation()) {
+        return false;
+    }
+
+    CoordinateXYZM a, b;
+    getCoordinateSequence()->getAt(getCoordinatePosition(), a);
+    other.getCoordinateSequence()->getAt(other.getCoordinatePosition(), b);
+
+    if (a.distance(b) > tol) {
+        return false;
+    }
+
+    if ((!std::isnan(a.z) || !std::isnan(b.z)) && !(std::abs(a.z - b.z) <= tol)) {
+        return false;
+    }
+
+    if ((!std::isnan(a.m) || !std::isnan(b.m)) && !(std::abs(a.m - b.m) <= tol)) {
+        return false;
+    }
+
+    return true;
+}
+
+
+#if 0
+std::pair<CircularArc, CircularArc>
+CircularArc::splitAtPoint(const CoordinateXY& q) const {
+    return std::make_pair(
+        CircularArc(p0(), q, getCenter(), getRadius(), getOrientation()),
+        CircularArc(q, p2(), getCenter(), getRadius(), getOrientation())
+    );
+}
+#endif
+
+std::string
+CircularArc::toString() const {
+    std::stringstream ss;
+    ss << "CIRCULARSTRING ";
+    if (m_seq->hasZ()) {
+        ss << "Z";
+    }
+    if (m_seq->hasM()) {
+        ss << "M";
+    }
+    if (m_seq->hasZ() || m_seq->hasM()) {
+        ss << " ";
+    }
+    ss << "(";
+    m_seq->applyAt(m_pos, [&ss](const auto& pt) {
+        ss << pt << ", " << *(&pt + 1) << ", " << *(&pt + 2);
+    });
+    ss << ")";
+    return ss.str();
+}
+
+}
diff --git a/src/geom/CircularString.cpp b/src/geom/CircularString.cpp
index 6e95aa035..361f56c56 100644
--- a/src/geom/CircularString.cpp
+++ b/src/geom/CircularString.cpp
@@ -69,7 +69,7 @@ CircularString::getLength() const
 
     double tot = 0;
     for (std::size_t i = 2; i < coords.size(); i += 2) {
-        auto len = CircularArc(coords[i-2], coords[i-1], coords[i]).getLength();
+        auto len = CircularArc(coords, i-2).getLength();
         tot += len;
     }
     return tot;
diff --git a/src/geom/CoordinateSequence.cpp b/src/geom/CoordinateSequence.cpp
index 1292c2bd1..865f97a44 100644
--- a/src/geom/CoordinateSequence.cpp
+++ b/src/geom/CoordinateSequence.cpp
@@ -608,6 +608,18 @@ CoordinateSequence::setPoints(const std::vector<CoordinateXY>& v)
     }
 }
 
+void
+CoordinateSequence::swap(std::size_t i, std::size_t j)
+{
+    using difference_type = decltype(m_vect)::difference_type;
+
+    if (i != j) {
+        std::swap_ranges(std::next(m_vect.begin(), static_cast<difference_type>(i*stride())),
+                         std::next(m_vect.begin(), static_cast<difference_type>(i+1) * stride()),
+                         std::next(m_vect.begin(), static_cast<difference_type>(j*stride())));
+    }
+}
+
 void
 CoordinateSequence::toVector(std::vector<Coordinate>& out) const
 {
diff --git a/src/noding/ArcIntersectionAdder.cpp b/src/noding/ArcIntersectionAdder.cpp
index 278228436..b6fb218c6 100644
--- a/src/noding/ArcIntersectionAdder.cpp
+++ b/src/noding/ArcIntersectionAdder.cpp
@@ -12,6 +12,7 @@
  *
  **********************************************************************/
 
+#include <geos/geom/CoordinateSequences.h>
 #include <geos/noding/ArcIntersectionAdder.h>
 #include <geos/noding/NodableArcString.h>
 #include <geos/noding/NodedSegmentString.h>
@@ -27,7 +28,7 @@ ArcIntersectionAdder::processIntersections(ArcString& e0, std::size_t segIndex0,
     }
 
     const geom::CircularArc& arc0 = e0.getArc(segIndex0);
-    const geom::CircularArc& arc1 = e1.getArc(segIndex0);
+    const geom::CircularArc& arc1 = e1.getArc(segIndex1);
 
     m_intersector.intersects(arc0, arc1);
 
@@ -37,8 +38,8 @@ ArcIntersectionAdder::processIntersections(ArcString& e0, std::size_t segIndex0,
 
     // TODO handle cocircular intersections
     for (std::uint8_t i = 0; i < m_intersector.getNumPoints(); i++) {
-        detail::down_cast<NodableArcString*>(&e0)->addIntersection(geom::CoordinateXYZM{m_intersector.getPoint(i)}, segIndex0);
-        detail::down_cast<NodableArcString*>(&e1)->addIntersection(geom::CoordinateXYZM{m_intersector.getPoint(i)}, segIndex1);
+        detail::down_cast<NodableArcString*>(&e0)->addIntersection(m_intersector.getPoint(i), segIndex0);
+        detail::down_cast<NodableArcString*>(&e1)->addIntersection(m_intersector.getPoint(i), segIndex1);
     }
 }
 
@@ -48,10 +49,10 @@ ArcIntersectionAdder::processIntersections(ArcString& e0, std::size_t segIndex0,
 // don't bother intersecting a segment with itself
 
     const geom::CircularArc& arc = e0.getArc(segIndex0);
-    const geom::CoordinateXY& q0 = e1.getCoordinate(segIndex1);
-    const geom::CoordinateXY& q1 = e1.getCoordinate(segIndex1 + 1);
 
-    m_intersector.intersects(arc, q0, q1);
+    // FIXME get useSegEndpoints from somewhere
+    constexpr bool useSegEndpoints = false;
+    m_intersector.intersects(arc, *e1.getCoordinates(), segIndex1, segIndex1 + 1, useSegEndpoints);
 
     if (m_intersector.getResult() == algorithm::CircularArcIntersector::NO_INTERSECTION) {
         return;
@@ -75,12 +76,17 @@ ArcIntersectionAdder::processIntersections(SegmentString& e0, std::size_t segInd
         return;
     }
 
+    m_intersector.intersects(*e0.getCoordinates(), segIndex0, segIndex0 + 1,
+                             *e1.getCoordinates(), segIndex1, segIndex1 + 1);
+
+#if 0
     const CoordinateXY& p0 = e0.getCoordinate(segIndex0);
     const CoordinateXY& p1 = e0.getCoordinate(segIndex0 + 1);
     const CoordinateXY& q0 = e1.getCoordinate(segIndex1);
     const CoordinateXY& q1 = e1.getCoordinate(segIndex1 + 1);
 
     m_intersector.intersects(p0, p1, q0, q1);
+#endif
 
     if (m_intersector.getResult() == algorithm::CircularArcIntersector::NO_INTERSECTION) {
         return;
diff --git a/include/geos/noding/PathString.h b/src/noding/ArcString.cpp
similarity index 51%
copy from include/geos/noding/PathString.h
copy to src/noding/ArcString.cpp
index e1a3ba4b3..6095414c2 100644
--- a/include/geos/noding/PathString.h
+++ b/src/noding/ArcString.cpp
@@ -12,24 +12,11 @@
  *
  **********************************************************************/
 
-#pragma once
-
-#include <geos/export.h>
-
-#include <cstddef>
+#include <geos/noding/ArcString.h>
 
 namespace geos::noding {
-
-/// A PathString represents a contiguous line/arc to the used as an input for
-/// noding. To access the coordinates, it is necessary to know whether they
-/// represent a set of line segments (SegmentString) or circular arcs (ArcString).
-class GEOS_DLL PathString {
-public:
-    virtual ~PathString() = default;
-
-    virtual std::size_t getSize() const = 0;
-
-    virtual double getLength() const = 0;
-};
-
-}
+ std::unique_ptr<geom::CoordinateSequence>
+ ArcString::releaseCoordinates() {
+  return std::move(m_seq);
+ }
+}
\ No newline at end of file
diff --git a/src/noding/GeometryNoder.cpp b/src/noding/GeometryNoder.cpp
index 46a5f0e04..0aaeeb89e 100644
--- a/src/noding/GeometryNoder.cpp
+++ b/src/noding/GeometryNoder.cpp
@@ -25,9 +25,13 @@
 #include <geos/geom/PrecisionModel.h>
 #include <geos/geom/CoordinateSequence.h>
 #include <geos/geom/GeometryFactory.h>
+#include <geos/geom/CircularString.h>
+#include <geos/geom/MultiCurve.h>
 #include <geos/geom/LineString.h>
 
 #include <geos/noding/IteratedNoder.h>
+#include <geos/noding/NodableArcString.h>
+#include <geos/noding/SimpleNoder.h>
 
 #include <geos/algorithm/LineIntersector.h>
 #include <geos/noding/IntersectionAdder.h>
@@ -46,9 +50,9 @@ namespace {
 /**
  * Add every linear element in a geometry into SegmentString vector
  */
-class SegmentStringExtractor: public geom::GeometryComponentFilter {
+class PathStringExtractor: public geom::GeometryComponentFilter {
 public:
-    SegmentStringExtractor(SegmentString::NonConstVect& to,
+    PathStringExtractor(std::vector<std::unique_ptr<PathString>> & to,
                            bool constructZ,
                            bool constructM)
         : _to(to)
@@ -59,20 +63,31 @@ public:
     void
     filter_ro(const geom::Geometry* g) override
     {
-        const geom::LineString* ls = dynamic_cast<const geom::LineString*>(g);
-        if(ls) {
+        if(const auto* ls = dynamic_cast<const geom::LineString*>(g)) {
             auto coord = ls->getSharedCoordinates();
-            SegmentString* ss = new NodedSegmentString(coord, _constructZ, _constructM, nullptr);
-            _to.push_back(ss);
+            // coord ownership transferred to SegmentString
+            auto ss = std::make_unique<NodedSegmentString>(coord, _constructZ, _constructM, nullptr);
+            _to.push_back(std::move(ss));
+        } else if (const auto* cs = dynamic_cast<const geom::CircularString*>(g)) {
+            auto coords = cs->getCoordinates();
+
+            // TODO: Store this vector in the CircularString ?
+            std::vector<geom::CircularArc> arcs;
+            for (std::size_t i = 0; i < coords->getSize() - 2; i += 2) {
+                arcs.emplace_back(*coords, i);
+            }
+
+            auto as = std::make_unique<NodableArcString>(std::move(arcs), std::move(coords), _constructZ, _constructM, nullptr);
+            _to.push_back(std::move(as));
         }
     }
 private:
-    SegmentString::NonConstVect& _to;
+    std::vector<std::unique_ptr<PathString>>& _to;
     bool _constructZ;
     bool _constructM;
 
-    SegmentStringExtractor(SegmentStringExtractor const&); /*= delete*/
-    SegmentStringExtractor& operator=(SegmentStringExtractor const&); /*= delete*/
+    PathStringExtractor(PathStringExtractor const&); /*= delete*/
+    PathStringExtractor& operator=(PathStringExtractor const&); /*= delete*/
 };
 
 }
@@ -89,14 +104,15 @@ GeometryNoder::node(const geom::Geometry& geom)
 /* public */
 GeometryNoder::GeometryNoder(const geom::Geometry& g)
     :
-    argGeom(g)
-{
-    util::ensureNoCurvedComponents(argGeom);
-}
+    argGeom(g),
+    argGeomHasCurves(g.hasCurvedComponents())
+{}
+
+GeometryNoder::~GeometryNoder() = default;
 
 /* private */
 std::unique_ptr<geom::Geometry>
-GeometryNoder::toGeometry(std::vector<std::unique_ptr<SegmentString>>& nodedEdges)
+GeometryNoder::toGeometry(std::vector<std::unique_ptr<PathString>>& nodedEdges) const
 {
     const geom::GeometryFactory* geomFact = argGeom.getFactory();
 
@@ -105,17 +121,30 @@ GeometryNoder::toGeometry(std::vector<std::unique_ptr<SegmentString>>& nodedEdge
     // Create a geometry out of the noded substrings.
     std::vector<std::unique_ptr<geom::Geometry>> lines;
     lines.reserve(nodedEdges.size());
-    for(auto& ss :  nodedEdges) {
-        const auto& coords = ss->getCoordinates();
 
-        // Check if an equivalent edge is known
-        OrientedCoordinateArray oca1(*coords);
-        if(ocas.insert(oca1).second) {
-            lines.push_back(geomFact->createLineString(coords));
+    bool resultArcs = false;
+    for(auto& path :  nodedEdges) {
+        if (const auto* ss = dynamic_cast<SegmentString*>(path.get())) {
+            const auto& coords = ss->getCoordinates();
+
+            // Check if an equivalent edge is known
+            OrientedCoordinateArray oca1(*coords);
+            if(ocas.insert(oca1).second) {
+                lines.push_back(geomFact->createLineString(coords));
+            }
+        } else {
+            resultArcs = true;
+            auto* as = dynamic_cast<ArcString*>(path.get());
+            // FIXME: check for duplicates
+            lines.push_back(geomFact->createCircularString(as->releaseCoordinates()));
         }
     }
 
-    return geomFact->createMultiLineString(std::move(lines));
+    if (resultArcs) {
+        return geomFact->createMultiCurve(std::move(lines));
+    } else {
+        return geomFact->createMultiLineString(std::move(lines));
+    }
 }
 
 /* public */
@@ -125,49 +154,45 @@ GeometryNoder::getNoded()
     if (argGeom.isEmpty())
         return argGeom.clone();
 
-    std::vector<SegmentString*> p_lineList;
-    extractSegmentStrings(argGeom, p_lineList);
+    std::vector<std::unique_ptr<PathString>> p_lineList;
+    extractPathStrings(argGeom, p_lineList);
 
-    Noder& p_noder = getNoder();
-    std::vector<std::unique_ptr<SegmentString>> nodedEdges;
+    ArcNoder& p_noder = getNoder();
+    std::vector<std::unique_ptr<PathString>> nodedEdges;
 
     try {
-        p_noder.computeNodes(p_lineList);
-        nodedEdges = p_noder.getNodedSubstrings();
+        p_noder.computePathNodes(PathString::toRawPointerVector(p_lineList));
+        nodedEdges = p_noder.getNodedPaths();
     }
     catch(const std::exception&) {
-        for(std::size_t i = 0, n = p_lineList.size(); i < n; ++i) {
-            delete p_lineList[i];
-        }
         throw;
     }
 
     std::unique_ptr<geom::Geometry> noded = toGeometry(nodedEdges);
 
-    for(auto* elem : p_lineList) {
-        delete elem;
-    }
-
     return noded;
 }
 
 /* private static */
 void
-GeometryNoder::extractSegmentStrings(const geom::Geometry& g,
-                                     SegmentString::NonConstVect& to)
+GeometryNoder::extractPathStrings(const geom::Geometry& g,
+                                  std::vector<std::unique_ptr<PathString>>& to)
 {
-    SegmentStringExtractor ex(to, g.hasZ(), g.hasM());
+    PathStringExtractor ex(to, g.hasZ(), g.hasM());
     g.apply_ro(&ex);
 }
 
 /* private */
-Noder&
+ArcNoder&
 GeometryNoder::getNoder()
 {
     if(! noder.get()) {
         const geom::PrecisionModel* pm = argGeom.getFactory()->getPrecisionModel();
-        IteratedNoder* in = new IteratedNoder(pm);
-        noder.reset(in);
+        if (argGeomHasCurves) {
+            noder = std::make_unique<IteratedNoder>(pm, []() { return std::make_unique<SimpleNoder>(); });
+        } else {
+            noder = std::make_unique<IteratedNoder>(pm);
+        }
     }
     return *noder;
 }
diff --git a/src/noding/IteratedNoder.cpp b/src/noding/IteratedNoder.cpp
index c88b76387..746828232 100644
--- a/src/noding/IteratedNoder.cpp
+++ b/src/noding/IteratedNoder.cpp
@@ -17,11 +17,14 @@
  *
  **********************************************************************/
 
+#include <functional>
 #include <sstream>
 #include <vector>
 
 #include <geos/profiler.h>
 #include <geos/util/TopologyException.h>
+#include <geos/noding/ArcIntersectionAdder.h>
+#include <geos/noding/ArcNoder.h>
 #include <geos/noding/IteratedNoder.h>
 #include <geos/noding/SegmentString.h>
 #include <geos/noding/MCIndexNoder.h>
@@ -31,47 +34,92 @@
 #define GEOS_DEBUG 0
 #endif
 
-using namespace geos::geom;
-
 namespace geos {
 namespace noding { // geos.noding
 
+IteratedNoder::IteratedNoder(const geom::PrecisionModel* newPm,
+                             std::function<std::unique_ptr<Noder>()> noderFunction)
+    :
+    pm(newPm),
+    li(pm),
+    maxIter(MAX_ITER),
+    m_noderFunction(noderFunction)
+{
+}
+
+std::unique_ptr<Noder>
+IteratedNoder::createDefaultNoder()
+{
+    return std::make_unique<MCIndexNoder>();
+}
+
+IteratedNoder::~IteratedNoder() = default;
+
 /* private */
 void
-IteratedNoder::node(const std::vector<SegmentString*>& segStrings,
+IteratedNoder::node(const std::vector<PathString*>& pathStrings,
                     int& numInteriorIntersections,
-                    CoordinateXY& intersectionPoint)
+                    geom::CoordinateXY& intersectionPoint)
 {
-    IntersectionAdder si(li);
-    MCIndexNoder noder;
-    noder.setSegmentIntersector(&si);
-    noder.computeNodes(segStrings);
-    auto updatedSegStrings = noder.getNodedSubstrings();
-    nodedSegStrings = std::move(updatedSegStrings);
-    numInteriorIntersections = si.numInteriorIntersections;
 
-    if (si.hasProperInteriorIntersection()) {
-        intersectionPoint = si.getProperIntersectionPoint();
+    auto noder = m_noderFunction();
+    if (auto* spn = dynamic_cast<SinglePassNoder*>(noder.get())) {
+        IntersectionAdder si(li);
+        spn->setSegmentIntersector(&si);
+        // TODO need to have previously checked that all inputs are SegmentStrings
+
+        std::vector<SegmentString*> segStrings(pathStrings.size());
+        for (size_t i = 0; i < pathStrings.size(); i++) {
+            segStrings[i] = detail::down_cast<SegmentString*>(pathStrings[i]);
+        }
+
+        noder->computeNodes(segStrings);
+
+        auto nodedSegStrings = noder->getNodedSubstrings();
+        nodedPaths.resize(nodedSegStrings.size());
+        for (size_t i = 0; i < nodedSegStrings.size(); i++) {
+            nodedPaths[i].reset(nodedSegStrings[i].release());
+        }
+
+        numInteriorIntersections = si.numInteriorIntersections;
+
+        if (si.hasProperInteriorIntersection()) {
+            intersectionPoint = si.getProperIntersectionPoint();
+        }
+    } else {
+        auto* arcNoder = detail::down_cast<ArcNoder*>(noder.get());
+        // FIXME aia should take a PrecsionModel / LineIntersector?
+        auto aia = std::make_unique<ArcIntersectionAdder>();
+        arcNoder->setArcIntersector(std::move(aia));
+        arcNoder->computePathNodes(pathStrings);
+        nodedPaths = arcNoder->getNodedPaths();
+
+        // FIXME use actual number!
+        numInteriorIntersections = 0;
+
+
+        // numInteriorIntersections?
+        // intesectionPoint?
     }
 }
 
 /* public */
 void
-IteratedNoder::computeNodes(const std::vector<SegmentString*>& segStrings)
+IteratedNoder::computePathNodes(const std::vector<PathString*>& paths)
 {
     int numInteriorIntersections;
     int nodingIterationCount = 0;
     int lastNodesCreated = -1;
-    CoordinateXY intersectionPoint = CoordinateXY::getNull();
+    geom::CoordinateXY intersectionPoint = geom::CoordinateXY::getNull();
 
     bool firstPass = true;
     do  {
         // NOTE: will change this.nodedSegStrings
         if (firstPass) {
-            node(segStrings, numInteriorIntersections, intersectionPoint);
+            node(paths, numInteriorIntersections, intersectionPoint);
             firstPass = false;
         } else {
-            auto nodingInput = SegmentString::toRawPointerVector(nodedSegStrings);
+            auto nodingInput = PathString::toRawPointerVector(nodedPaths);
             node(nodingInput, numInteriorIntersections, intersectionPoint);
         }
 
diff --git a/src/noding/NodableArcString.cpp b/src/noding/NodableArcString.cpp
new file mode 100644
index 000000000..331669349
--- /dev/null
+++ b/src/noding/NodableArcString.cpp
@@ -0,0 +1,133 @@
+/**********************************************************************
+ *
+ * GEOS - Geometry Engine Open Source
+ * http://geos.osgeo.org
+ *
+ * Copyright (C) 2025 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/noding/NodableArcString.h>
+
+namespace geos::noding {
+
+static double
+pseudoAngleDiffCCW(double paStart, double pa) {
+    double diff = pa - paStart;
+
+    if (diff <= 0) {
+        diff += 4;
+    }
+
+    return diff;
+}
+
+NodableArcString::NodableArcString(std::vector<geom::CircularArc> arcs, std::unique_ptr<geom::CoordinateSequence> coords, bool constructZ, bool constructM, void* context) :
+    ArcString(std::move(arcs), std::move(coords), context),
+    m_constructZ(constructZ),
+    m_constructM(constructM)
+{
+}
+
+
+//std::unique_ptr<ArcString> clone() const {
+//
+//}
+
+std::unique_ptr<ArcString>
+NodableArcString::getNoded() {
+
+    auto dstSeq = std::make_unique<geom::CoordinateSequence>(0, m_constructZ, m_constructM);
+
+        //if (m_adds.empty()) {
+        //    return clone();
+        //}
+
+        std::vector<geom::CircularArc> arcs;
+        for (size_t i = 0; i < m_arcs.size(); i++) {
+            if (const auto it = m_adds.find(i); it == m_adds.end()) {
+                // No nodes added, just copy the coordinates into the sequence.
+                const geom::CoordinateSequence* srcSeq = m_arcs[i].getCoordinateSequence();
+                std::size_t srcPos = m_arcs[i].getCoordinatePosition();
+                std::size_t dstPos = dstSeq->getSize();
+                dstSeq->add(*srcSeq, srcPos, srcPos + 2, false);
+                arcs.emplace_back(*dstSeq, dstPos);
+            } else {
+                std::vector<geom::CoordinateXYZM>& splitPoints = it->second;
+
+                // TODO check split point actually inside arc?
+
+                const geom::CircularArc& toSplit = m_arcs[i];
+                const geom::CoordinateXY& center = toSplit.getCenter();
+                const double radius = toSplit.getRadius();
+                const int orientation = toSplit.getOrientation();
+                const bool isCCW = orientation == algorithm::Orientation::COUNTERCLOCKWISE;
+                const double paStart = geom::Quadrant::pseudoAngle(center, toSplit.p0());
+
+                std::sort(splitPoints.begin(), splitPoints.end(), [&center, paStart, isCCW](const auto& p0, const auto& p1) {
+                    double pa0 = geom::Quadrant::pseudoAngle(center, p0);
+                    double pa1 = geom::Quadrant::pseudoAngle(center, p1);
+
+                    if (isCCW) {
+                        return pseudoAngleDiffCCW(paStart, pa0) < pseudoAngleDiffCCW(paStart, pa1);
+                    } else {
+                        return pseudoAngleDiffCCW(paStart, pa0) > pseudoAngleDiffCCW(paStart, pa1);
+                    }
+                });
+
+#if 0
+                std::cout << "Splitting " << toSplit.toString() << " " << (isCCW ? "CCW" : "CW") << " paStart " << paStart << " paStop " << geom::Quadrant::pseudoAngle(center, toSplit.p2()) << std::endl;
+                for (const auto& splitPt : splitPoints)
+                {
+                    const double pa = geom::Quadrant::pseudoAngle(center, splitPt);
+                    std::cout << "  " << splitPt.toString() << "  (pa " << pa << " diff " << pseudoAngleDiffCCW(paStart, pa) << ")" << std::endl;
+                }
+#endif
+
+                // Add first point of split arc
+                std::size_t dstPos = dstSeq->getSize();
+                dstSeq->add(*toSplit.getCoordinateSequence(), toSplit.getCoordinatePosition(), toSplit.getCoordinatePosition());
+                geom::CoordinateXYZM p0, p2;
+                dstSeq->getAt(dstPos, p0);
+
+                // Add intermediate points of split arc
+                for (const auto& splitPoint : splitPoints) {
+                    if (!arcs.empty() && splitPoint.equals2D(arcs.back().p2())) {
+                        continue;
+                    }
+
+                    geom::CoordinateXYZM midpoint(algorithm::CircularArcs::getMidpoint(p0, splitPoint, center, radius, isCCW));
+                    midpoint.z = (p0.z + splitPoint.z) / 2;
+                    midpoint.m = (p0.m + splitPoint.m) / 2;
+
+                    dstSeq->add(midpoint);
+                    dstSeq->add(splitPoint);
+
+                    p0 = splitPoint;
+
+                    arcs.emplace_back(*dstSeq, dstPos, center, radius, orientation);
+                    dstPos = dstSeq->getSize() - 1;
+                }
+
+                // Add last point of split arc
+                toSplit.getCoordinateSequence()->getAt(toSplit.getCoordinatePosition() + 2, p2);
+
+                geom::CoordinateXYZM midpoint(algorithm::CircularArcs::getMidpoint(p0, p2, center, radius, isCCW));
+                midpoint.z = (p0.z + p2.z) / 2;
+                midpoint.m = (p0.m + p2.m) / 2;
+
+                dstSeq->add(midpoint);
+                dstSeq->add(p2);
+                arcs.emplace_back(*dstSeq, dstPos, center, radius, orientation);
+            }
+        }
+
+        return std::make_unique<NodableArcString>(std::move(arcs), std::move(dstSeq), m_constructZ, m_constructM, nullptr);
+    }
+
+}
\ No newline at end of file
diff --git a/include/geos/noding/PathString.h b/src/noding/PathString.cpp
similarity index 52%
copy from include/geos/noding/PathString.h
copy to src/noding/PathString.cpp
index e1a3ba4b3..af4760a09 100644
--- a/include/geos/noding/PathString.h
+++ b/src/noding/PathString.cpp
@@ -12,24 +12,19 @@
  *
  **********************************************************************/
 
-#pragma once
-
-#include <geos/export.h>
-
-#include <cstddef>
+#include <geos/noding/PathString.h>
+#include <geos/util.h>
 
 namespace geos::noding {
 
-/// A PathString represents a contiguous line/arc to the used as an input for
-/// noding. To access the coordinates, it is necessary to know whether they
-/// represent a set of line segments (SegmentString) or circular arcs (ArcString).
-class GEOS_DLL PathString {
-public:
-    virtual ~PathString() = default;
-
-    virtual std::size_t getSize() const = 0;
-
-    virtual double getLength() const = 0;
-};
-
+std::vector<PathString*>
+PathString::toRawPointerVector(const std::vector<std::unique_ptr<PathString>> & pathStrings)
+{
+    std::vector<PathString*> ret(pathStrings.size());
+    for (std::size_t i = 0; i < pathStrings.size(); i++) {
+        ret[i] = pathStrings[i].get();
+    }
+    return ret;
 }
+
+}
\ No newline at end of file
diff --git a/src/noding/SegmentString.cpp b/src/noding/SegmentString.cpp
index b5a56c70f..4416116b8 100644
--- a/src/noding/SegmentString.cpp
+++ b/src/noding/SegmentString.cpp
@@ -62,6 +62,15 @@ SegmentString::toRawPointerVector(const std::vector<std::unique_ptr<SegmentStrin
     return ret;
 }
 
+std::vector<SegmentString*>
+SegmentString::toRawPointerVector(const std::vector<std::unique_ptr<PathString>> & segStrings) {
+    std::vector<SegmentString*> ret(segStrings.size());
+    for (std::size_t i = 0; i < segStrings.size(); i++) {
+        ret[i] = detail::down_cast<SegmentString*>(segStrings[i].get());
+    }
+    return ret;
+}
+
 } // namespace geos.noding
 } // namespace geos
 
diff --git a/src/noding/SimpleNoder.cpp b/src/noding/SimpleNoder.cpp
index 7fc2007f3..496de6070 100644
--- a/src/noding/SimpleNoder.cpp
+++ b/src/noding/SimpleNoder.cpp
@@ -67,7 +67,6 @@ SimpleNoder::computePathNodes(const std::vector<PathString*>& inputPathStrings)
 
     for (auto* edge0: m_pathStrings) {
         for (auto* edge1: m_pathStrings) {
-            // TODO skip processing against self?
             computeIntersects(*edge0, *edge1);
         }
     }
diff --git a/tests/unit/algorithm/CircularArcIntersectorTest.cpp b/tests/unit/algorithm/CircularArcIntersectorTest.cpp
index 2f9e51bdc..dc07a91aa 100644
--- a/tests/unit/algorithm/CircularArcIntersectorTest.cpp
+++ b/tests/unit/algorithm/CircularArcIntersectorTest.cpp
@@ -5,9 +5,16 @@
 #include <geos/constants.h>
 #include <variant>
 
+#include "geos/util.h"
+
 using geos::algorithm::CircularArcIntersector;
 using geos::algorithm::Orientation;
+using geos::geom::Ordinate;
+using geos::geom::Coordinate;
+using geos::geom::CoordinateSequence;
 using geos::geom::CoordinateXY;
+using geos::geom::CoordinateXYM;
+using geos::geom::CoordinateXYZM;
 using geos::geom::CircularArc;
 using geos::MATH_PI;
 
@@ -15,102 +22,236 @@ namespace tut {
 
 struct test_circulararcintersector_data {
 
-    using ArcOrPoint = std::variant<CoordinateXY, CircularArc>;
+    using XY = CoordinateXY;
+    using XYZ = Coordinate;
+    using XYM = CoordinateXYM;
+    using XYZM = CoordinateXYZM;
+
+    static constexpr double NaN = geos::DoubleNotANumber;
+
+    using ArcOrPoint = std::variant<XY, XYZ, XYM, XYZM, CircularArc>;
 
     static std::string to_string(CircularArcIntersector::intersection_type t)
     {
         switch (t) {
-        case geos::algorithm::CircularArcIntersector::NO_INTERSECTION:
+        case CircularArcIntersector::NO_INTERSECTION:
             return "no intersection";
-        case geos::algorithm::CircularArcIntersector::ONE_POINT_INTERSECTION:
+        case CircularArcIntersector::ONE_POINT_INTERSECTION:
             return "one-point intersection";
-        case geos::algorithm::CircularArcIntersector::TWO_POINT_INTERSECTION:
+        case CircularArcIntersector::TWO_POINT_INTERSECTION:
             return "two-point intersection";
-        case geos::algorithm::CircularArcIntersector::COCIRCULAR_INTERSECTION:
+        case CircularArcIntersector::COCIRCULAR_INTERSECTION:
             return "cocircular intersection";
-            break;
         }
 
         return "";
     }
 
-    static std::string toWKT(const CoordinateXY& pt)
+    static std::string toWKT(const CoordinateXYZM& pt)
     {
-        return "POINT (" + pt.toString() + ")";
+        const bool hasZ = !std::isnan(pt.z);
+        const bool hasM = !std::isnan(pt.m);
+
+        std::stringstream ss;
+        ss << "POINT ";
+        if (hasZ) {
+            ss << "Z";
+        }
+        if (hasM) {
+            ss << "M";
+        }
+        if (hasZ || hasM) {
+            ss << " ";
+        }
+        ss << "(";
+        ss << pt.x << " " << pt.y;
+        if (hasZ) {
+            ss << " " << pt.z;
+        }
+        if (hasM) {
+            ss << " " << pt.m;
+        }
+        ss << ")";
+
+        return ss.str();
     }
 
     static std::string toWKT(const CircularArc& arc)
     {
-        return "CIRCULARSTRING (" + arc.p0.toString() + ", " + arc.p1.toString() + ", " + arc.p2.toString() + ")";
+        return arc.toString();
     }
 
-    static std::string toWKT(const geos::geom::LineSegment& seg)
+    static std::string toWKT(const CoordinateSequence & seg)
     {
-        return "LINESTRING (" + seg.p0.toString() + ", " + seg.p1.toString() + ")";
+        std::stringstream ss;
+
+        ss << "LINESTRING (";
+        seg.applyAt(0, [&ss](const auto& pt) {
+            ss << pt << ", " << *(&pt + 1);
+        });
+        ss << ")";
+
+        return ss.str();
     }
 
-    static void checkIntersection(CoordinateXY p0, CoordinateXY p1, CoordinateXY p2,
-                                  CoordinateXY q0, CoordinateXY q1, CoordinateXY q2,
+    template<typename C1, typename C2>
+    static void checkIntersection(C1 p0, C1 p1, C1 p2,
+                                  C2 q0, C2 q1, C2 q2,
                                   CircularArcIntersector::intersection_type result,
-                                  ArcOrPoint i0 = CoordinateXY::getNull(),
-                                  ArcOrPoint i1 = CoordinateXY::getNull())
+                                  const ArcOrPoint& i0 = CoordinateXYZM::getNull(),
+                                  const ArcOrPoint& i1 = CoordinateXYZM::getNull())
     {
-        CircularArc a0(p0, p1, p2);
-        CircularArc a1(q0, q1, q2);
+        CoordinateSequence cs1(3, C1::template has<Ordinate::Z>(), C1::template has<Ordinate::M>());
+        cs1.setAt(p0, 0);
+        cs1.setAt(p1, 1);
+        cs1.setAt(p2, 2);
+
+        CoordinateSequence cs2(3, C2::template has<Ordinate::Z>(), C2::template has<Ordinate::M>());
+        cs2.setAt(q0, 0);
+        cs2.setAt(q1, 1);
+        cs2.setAt(q2, 2);
+
+        const CircularArc a0(cs1, 0);
+        const CircularArc a1(cs2, 0);
 
         checkIntersection(a0, a1, result, i0, i1);
     }
 
-    static void checkIntersection(CoordinateXY p0, CoordinateXY p1, CoordinateXY p2,
-                                  CoordinateXY q0, CoordinateXY q1,
-                                  CircularArcIntersector::intersection_type result,
-                                  CoordinateXY i0 = CoordinateXY::getNull(),
-                                  CoordinateXY i1 = CoordinateXY::getNull())
+    template<typename C1, typename C2>
+    static void checkIntersectionArcSeg(C1 p0, C1 p1, C1 p2,
+                                        C2 q0, C2 q1,
+                                        CircularArcIntersector::intersection_type result,
+                                        const ArcOrPoint& i0 = CoordinateXYZM::getNull(),
+                                        const ArcOrPoint& i1 = CoordinateXYZM::getNull())
     {
-        CircularArc a(p0, p1, p2);
-        geos::geom::LineSegment s(geos::geom::Coordinate{q0}, geos::geom::Coordinate{q1});
+        CoordinateSequence cs1(3, C1::template has<Ordinate::Z>(), C1::template has<Ordinate::M>());
+        cs1.setAt(p0, 0);
+        cs1.setAt(p1, 1);
+        cs1.setAt(p2, 2);
 
-        checkIntersection(a, s, result, i0, i1);
+        const CircularArc arc(cs1, 0);
+
+        CoordinateSequence seg(2, C2::template has<Ordinate::Z>(), C2::template has<Ordinate::M>());
+        seg.setAt(q0, 0);
+        seg.setAt(q1, 1);
+
+        checkIntersection(arc, seg, result, i0, i1);
     }
 
-    static bool pointWithinTolerance(const CoordinateXY& actual, const CoordinateXY& expected, double tol)
+    template<typename C1, typename C2>
+    static void checkIntersectionSegArc(C1 p0, C1 p1,
+                                        C2 q0, C2 q1, C2 q2,
+                                        CircularArcIntersector::intersection_type result,
+                                        const ArcOrPoint& i0 = CoordinateXYZM::getNull(),
+                                        const ArcOrPoint& i1 = CoordinateXYZM::getNull())
     {
+        CoordinateSequence seg(2, C1::template has<Ordinate::Z>(), C1::template has<Ordinate::M>());
+        seg.setAt(p0, 0);
+        seg.setAt(p1, 1);
+
+
+        CoordinateSequence arcSeq(3, C2::template has<Ordinate::Z>(), C2::template has<Ordinate::M>());
+        arcSeq.setAt(q0, 0);
+        arcSeq.setAt(q1, 1);
+        arcSeq.setAt(q2, 2);
+        const CircularArc arc(arcSeq, 0);
+
+        checkIntersection(arc, seg, result, i0, i1, true);
+    }
+
+    template<typename C1, typename C2>
+    static void checkIntersectionSegSeg(C1 p0, C1 p1,
+                                        C2 q0, C2 q1,
+                                        CircularArcIntersector::intersection_type result,
+                                        const ArcOrPoint& i0 = CoordinateXYZM::getNull(),
+                                        const ArcOrPoint& i1 = CoordinateXYZM::getNull())
+    {
+        CoordinateSequence seg0(2, C1::template has<Ordinate::Z>(), C1::template has<Ordinate::M>());
+        seg0.setAt(p0, 0);
+        seg0.setAt(p1, 1);
+
+        CoordinateSequence seg1(2, C2::template has<Ordinate::Z>(), C2::template has<Ordinate::M>());
+        seg1.setAt(q0, 0);
+        seg1.setAt(q1, 1);
+
+        checkIntersection(seg0, seg1, result, i0, i1, true);
+    }
+
+
+    static bool pointWithinTolerance(const CoordinateXYZM& actual, const CoordinateXYZM& expected, double tol)
+    {
+        if (std::isnan(actual.z) != std::isnan(expected.z)) {
+            return false;
+        }
+
+        if (std::isnan(actual.m) != std::isnan(expected.m)) {
+            return false;
+        }
+
+        if (!std::isnan(expected.z) && std::abs(actual.z - expected.z) > tol * std::abs(expected.z)) {
+            return false;
+        }
+
+        if (!std::isnan(expected.m) && std::abs(actual.m - expected.m) > tol * std::abs(expected.m)) {
+            return false;
+        }
+
         if (actual.distance(expected) < tol) {
             return true;
         }
 
-        return std::abs(actual.x - expected.x) < tol * std::abs(actual.x) &&
-            std::abs(actual.y - expected.y) < tol * std::abs(actual.y);
+        if (std::abs(actual.x - expected.x) > tol * std::abs(expected.x)) {
+            return false;
+        }
+
+        if (std::abs(actual.y - expected.y) > tol * std::abs(expected.y)) {
+            return false;
+        }
+
+        return true;
     }
 
-    template<typename CircularArcOrLineSegment>
-    static void checkIntersection(const CircularArc& a0,
-                                  const CircularArcOrLineSegment& a1,
+    template<typename T1, typename T2>
+    static void checkIntersection(const T1& a0,
+                                  const T2& a1,
                                   CircularArcIntersector::intersection_type result,
-                                  ArcOrPoint p0 = CoordinateXY::getNull(),
-                                  ArcOrPoint p1 = CoordinateXY::getNull())
+                                  const ArcOrPoint& p0 = CoordinateXYZM::getNull(),
+                                  const ArcOrPoint& p1 = CoordinateXYZM::getNull(),
+                                  bool useSegEndpoints=false)
     {
         CircularArcIntersector cai;
-        cai.intersects(a0, a1);
+        if constexpr (std::is_same_v<T1, CircularArc>)
+            if constexpr (std::is_same_v<T2, CircularArc>) {
+                geos::ignore_unused_variable_warning(useSegEndpoints); // needed for gcc 10
+                cai.intersects(a0, a1);
+            } else {
+                cai.intersects(a0, a1, 0, 1, useSegEndpoints);
+        } else {
+            static_assert(std::is_same_v<T1, CoordinateSequence>);
+            geos::ignore_unused_variable_warning(useSegEndpoints); // needed for gcc 10
+            cai.intersects(a0, 0, 1, a1, 0, 1);
+        }
 
         ensure_equals("incorrect intersection type between " + toWKT(a0) + " and " + toWKT(a1), to_string(cai.getResult()), to_string(result));
 
-        std::vector<CoordinateXY> expectedPoints;
+        std::vector<CoordinateXYZM> expectedPoints;
         std::vector<CircularArc> expectedArcs;
 
         for (const auto& intersection : { p0, p1 }) {
-            if (std::holds_alternative<CoordinateXY>(intersection)) {
-                const CoordinateXY& pt = std::get<CoordinateXY>(intersection);
-                if (!pt.isNull()) {
-                    expectedPoints.push_back(pt);
+            std::visit([&expectedArcs, &expectedPoints](const auto& isect) {
+                using IntersectionType = std::decay_t<decltype(isect)>;
+
+                if constexpr (std::is_same_v<IntersectionType, CircularArc>) {
+                    expectedArcs.push_back(isect);
+                } else {
+                    if (!isect.isNull()) {
+                        expectedPoints.push_back(CoordinateXYZM(isect));
+                    }
                 }
-            }
-            else {
-                expectedArcs.push_back(std::get<CircularArc>(intersection));
-            }
+            }, intersection);
         }
 
-        std::vector<CoordinateXY> actualPoints;
+        std::vector<CoordinateXYZM> actualPoints;
         std::vector<CircularArc> actualArcs;
 
         for (std::uint8_t i = 0; i < cai.getNumPoints(); i++) {
@@ -122,12 +263,11 @@ struct test_circulararcintersector_data {
         }
 
         auto compareArcs = [](const CircularArc& a, const CircularArc& b) {
-            int cmp;
-            cmp = a.p0.compareTo(b.p0);
+            int cmp = a.p0().compareTo(b.p0());
             if (cmp != 0) {
                 return cmp == -1;
             }
-            cmp = a.p2.compareTo(b.p2);
+            cmp = a.p2().compareTo(b.p2());
             if (cmp != 0) {
                 return cmp == -1;
             }
@@ -168,15 +308,23 @@ struct test_circulararcintersector_data {
                     equal = false;
                 }
 
-                if (!pointWithinTolerance(actualArcs[i].getCenter(), expectedArcs[i].getCenter(), eps)) {
+                if (!pointWithinTolerance(XYZM(actualArcs[i].getCenter()), XYZM(expectedArcs[i].getCenter()), eps)) {
                     equal = false;
                 }
 
-                if (!pointWithinTolerance(actualArcs[i].p0, expectedArcs[i].p0, eps)) {
+                XYZM actual0, expected0;
+                actualArcs[i].getCoordinateSequence()->getAt(actualArcs[i].getCoordinatePosition(), actual0);
+                expectedArcs[i].getCoordinateSequence()->getAt(expectedArcs[i].getCoordinatePosition(), expected0);
+
+                if (!pointWithinTolerance(actual0, expected0, eps)) {
                     equal = false;
                 }
 
-                if (!pointWithinTolerance(actualArcs[i].p2, expectedArcs[i].p2, eps)) {
+                XYZM actual2, expected2;
+                actualArcs[i].getCoordinateSequence()->getAt(actualArcs[i].getCoordinatePosition(), actual2);
+                expectedArcs[i].getCoordinateSequence()->getAt(expectedArcs[i].getCoordinatePosition(), expected2);
+
+                if (!pointWithinTolerance(actual2, expected2, eps)) {
                     equal = false;
                 }
             }
@@ -214,17 +362,17 @@ struct test_circulararcintersector_data {
             expected += toWKT(arc);
         }
 
-        ensure_equals(actual, expected);
+        ensure_equals("incorrect intersection loc between " + toWKT(a0) + " and " + toWKT(a1), actual, expected);
     }
 
-    const CoordinateXY _NW = { -std::sqrt(2)/2, std::sqrt(2)/2 };
-    const CoordinateXY _N =  { 0, 1};
-    const CoordinateXY _NE = {  std::sqrt(2)/2, std::sqrt(2)/2 };
-    const CoordinateXY _E =  { 1, 0};
-    const CoordinateXY _SE = {  std::sqrt(2)/2, -std::sqrt(2)/2 };
-    const CoordinateXY _S =  { 0, -1};
-    const CoordinateXY _SW = { -std::sqrt(2)/2, -std::sqrt(2)/2 };
-    const CoordinateXY _W =  { -1, 0};
+    const CoordinateXY NW_ = { -std::sqrt(2)/2, std::sqrt(2)/2 };
+    const CoordinateXY N_ =  { 0, 1};
+    const CoordinateXY NE_ = {  std::sqrt(2)/2, std::sqrt(2)/2 };
+    const CoordinateXY E_ =  { 1, 0};
+    const CoordinateXY SE_ = {  std::sqrt(2)/2, -std::sqrt(2)/2 };
+    const CoordinateXY S_ =  { 0, -1};
+    const CoordinateXY SW_ = { -std::sqrt(2)/2, -std::sqrt(2)/2 };
+    const CoordinateXY W_ =  { -1, 0};
 };
 
 using group = test_group<test_circulararcintersector_data>;
@@ -238,10 +386,12 @@ void object::test<1>()
 {
     set_test_name("interior/interior intersection (one point)");
 
-    checkIntersection({0, 0}, {1, std::sqrt(3)}, {2, 2},
-    {0, 2}, {1, std::sqrt(3)}, {2, 0},
-    CircularArcIntersector::ONE_POINT_INTERSECTION,
-    CoordinateXY{1, std::sqrt(3)});
+    checkIntersection(
+        XY{0, 0}, XY{1, std::sqrt(3)}, XY{2, 2},
+        XY{0, 2}, XY{1, std::sqrt(3)}, XY{2, 0},
+        CircularArcIntersector::ONE_POINT_INTERSECTION,
+        XY{1, std::sqrt(3)}
+    );
 }
 
 template<>
@@ -251,11 +401,13 @@ void object::test<2>()
     set_test_name("interior/interior intersection (two points)");
 
     // result from CGAL 5.4
-    checkIntersection({0, 0}, {2, 2}, {4, 0},
-    {0, 1}, {2, -1}, {4, 1},
-    CircularArcIntersector::TWO_POINT_INTERSECTION,
-    CoordinateXY{0.0635083268962914893, 0.5},
-    CoordinateXY{3.93649167310370851, 0.5});
+    checkIntersection(
+        XY{0, 0}, XY{2, 2}, XY{4, 0},
+        XY{0, 1}, XY{2, -1}, XY{4, 1},
+        CircularArcIntersector::TWO_POINT_INTERSECTION,
+        XY{0.0635083268962914893, 0.5},
+        XY{3.93649167310370851, 0.5}
+    );
 }
 
 template<>
@@ -264,10 +416,12 @@ void object::test<3>()
 {
     set_test_name("single endpoint-endpoint intersection");
 
-    checkIntersection({0, 0}, {1, 1}, {2, 0},
-    {2, 0}, {3, -1}, {4, 0},
-    CircularArcIntersector::ONE_POINT_INTERSECTION,
-    CoordinateXY{2, 0});
+    checkIntersection(
+        XY{0, 0}, XY{1, 1}, XY{2, 0},
+        XY{2, 0}, XY{3, -1}, XY{4, 0},
+        CircularArcIntersector::ONE_POINT_INTERSECTION,
+        XY{2, 0}
+    );
 }
 
 template<>
@@ -276,10 +430,12 @@ void object::test<4>()
 {
     set_test_name("single interior-interior intersection at point of tangency");
 
-    checkIntersection({0, 0}, {1, 1}, {2, 0},
-    {0, 2}, {1, 1}, {2, 2},
-    CircularArcIntersector::ONE_POINT_INTERSECTION,
-    CoordinateXY{1, 1});
+    checkIntersection(
+        XY{0, 0}, XY{1, 1}, XY{2, 0},
+        XY{0, 2}, XY{1, 1}, XY{2, 2},
+        CircularArcIntersector::ONE_POINT_INTERSECTION,
+        XY{1, 1}
+    );
 }
 
 template<>
@@ -288,9 +444,11 @@ void object::test<5>()
 {
     set_test_name("supporting circles intersect but arcs do not");
 
-    checkIntersection({0, 0}, {2, 2}, {4, 0},
-    {1, 1}, {0, -1}, {-1, 1},
-    CircularArcIntersector::NO_INTERSECTION);
+    checkIntersection(
+        XY{0, 0}, XY{2, 2}, XY{4, 0},
+        XY{1, 1}, XY{0, -1}, XY{-1, 1},
+      CircularArcIntersector::NO_INTERSECTION
+    );
 
 }
 
@@ -300,9 +458,11 @@ void object::test<6>()
 {
     set_test_name("one circle contained within other");
 
-    checkIntersection({0, 0}, {4, 4}, {8, 0},
-    {2, 0}, {4, 2}, {6, 0},
-    CircularArcIntersector::NO_INTERSECTION);
+    checkIntersection(
+        XY{0, 0}, XY{4, 4}, XY{8, 0},
+        XY{2, 0}, XY{4, 2}, XY{6, 0},
+        CircularArcIntersector::NO_INTERSECTION
+    );
 }
 
 template<>
@@ -311,10 +471,12 @@ void object::test<7>()
 {
     set_test_name("cocircular with double endpoint intersection");
 
-    checkIntersection({0, 0}, {1, 1}, {2, 0},
-    {0, 0}, {1, -1}, {2, 0},
-    CircularArcIntersector::TWO_POINT_INTERSECTION,
-    CoordinateXY{0, 0}, CoordinateXY{2, 0});
+    checkIntersection(
+        XY{0, 0}, XY{1, 1}, XY{2, 0},
+        XY{0, 0}, XY{1, -1}, XY{2, 0},
+        CircularArcIntersector::TWO_POINT_INTERSECTION,
+        XY{0, 0}, XY{2, 0}
+    );
 }
 
 template<>
@@ -323,10 +485,12 @@ void object::test<8>()
 {
     set_test_name("cocircular with single endpoint intersection");
 
-    checkIntersection({-2, 0}, {0, 2}, {2, 0},
-    {0, -2}, {std::sqrt(2), -std::sqrt(2)}, {2, 0},
-    CircularArcIntersector::ONE_POINT_INTERSECTION,
-    CoordinateXY{2, 0});
+    checkIntersection(
+        XY{-2, 0}, XY{0, 2}, XY{2, 0},
+        XY{0, -2}, XY{std::sqrt(2), -std::sqrt(2)}, XY{2, 0},
+        CircularArcIntersector::ONE_POINT_INTERSECTION,
+        XY{2, 0}
+    );
 }
 
 template<>
@@ -335,8 +499,8 @@ void object::test<9>()
 {
     set_test_name("cocircular disjoint");
 
-    checkIntersection(_NW, _N, _NE,
-                      _SW, _S, _SE,
+    checkIntersection(NW_, N_, NE_,
+                      SW_, S_, SE_,
                       CircularArcIntersector::NO_INTERSECTION);
 }
 
@@ -346,10 +510,12 @@ void object::test<10>()
 {
     set_test_name("cocircular with single arc intersection (clockwise)");
 
-    checkIntersection({-5, 0}, {0, 5}, {5, 0}, // CW
-    {-4, 3}, {0, 5}, {4, 3}, // CW
-    CircularArcIntersector::COCIRCULAR_INTERSECTION,
-    CircularArc{{-4, 3}, {0, 5}, {4, 3}}); // CW
+    checkIntersection(
+        XY{-5, 0}, XY{0, 5}, XY{5, 0}, // CW
+        XY{-4, 3}, XY{0, 5}, XY{4, 3}, // CW
+        CircularArcIntersector::COCIRCULAR_INTERSECTION,
+        CircularArc::create(XY{-4, 3}, {0, 5}, {4, 3})
+    ); // CW
 }
 
 template<>
@@ -358,10 +524,12 @@ void object::test<11>()
 {
     set_test_name("cocircular with single arc intersection (counter-clockwise)");
 
-    checkIntersection({5, 0},  {0, 5}, {-5, 0}, // CCW
-    {-4, 3}, {0, 5}, {4, 3},  // CW
-    CircularArcIntersector::COCIRCULAR_INTERSECTION,
-    CircularArc{{4, 3}, {0, 5}, {-4, 3}}); // CCW
+    checkIntersection(
+        XY{5, 0}, XY{0, 5}, XY{-5, 0}, // CCW
+        XY{-4, 3}, XY{0, 5}, XY{4, 3}, // CW
+        CircularArcIntersector::COCIRCULAR_INTERSECTION,
+        CircularArc::create(XY{4, 3}, {0, 5}, {-4, 3})
+    ); // CCW
 }
 
 template<>
@@ -370,11 +538,13 @@ void object::test<12>()
 {
     set_test_name("cocircular with arc and point intersections");
 
-    checkIntersection({-5, 0}, {0, 5}, {5, 0},
-    {5, 0}, {0, -5}, {0, 5},
-    CircularArcIntersector::COCIRCULAR_INTERSECTION,
-    CircularArc{{-5, 0}, {-5*std::sqrt(2)/2, 5*std::sqrt(2)/2}, {0, 5}},
-    CoordinateXY{5, 0});
+    checkIntersection(
+        XY{-5, 0}, XY{0, 5}, XY{5, 0},
+        XY{5, 0}, XY{0, -5}, XY{0, 5},
+      CircularArcIntersector::COCIRCULAR_INTERSECTION,
+     CircularArc::create(XY{-5, 0}, {-5 * std::sqrt(2) / 2, 5 * std::sqrt(2) / 2}, {0, 5}),
+     XY{5, 0}
+    );
 }
 
 template<>
@@ -383,11 +553,13 @@ void object::test<13>()
 {
     set_test_name("cocircular with two arc intersections");
 
-    checkIntersection({-5, 0}, {0, 5}, {5, 0},
-    {3, 4}, {0, -5}, {-3, 4},
-    CircularArcIntersector::COCIRCULAR_INTERSECTION,
-    CircularArc{{3, 4}, {4.4721359549995796, 2.2360679774997898}, {5, 0}},
-    CircularArc{{-5, 0}, {-4.4721359549995796, 2.2360679774997907}, {-3, 4}});
+    checkIntersection(
+        XY{-5, 0}, XY{0, 5}, XY{5, 0},
+        XY{3, 4}, XY{0, -5}, XY{-3, 4},
+      CircularArcIntersector::COCIRCULAR_INTERSECTION,
+     CircularArc::create(XY{3, 4}, {4.4721359549995796, 2.2360679774997898}, {5, 0}),
+     CircularArc::create(XY{-5, 0}, {-4.4721359549995796, 2.2360679774997907}, {-3, 4})
+    );
 }
 
 template<>
@@ -396,15 +568,15 @@ void object::test<20>()
 {
     set_test_name("arc - degenerate arc with single interior intersection");
 
-    checkIntersection({0, 0}, {2, 2}, {4, 0}, // CW arc
-                      {-1, -4}, {1, 0}, {3, 4}, // degenerate arc
+    checkIntersection(XY{0, 0}, XY{2, 2}, XY{4, 0}, // CW arc
+                      XY{-1, -4}, XY{1, 0}, XY{3, 4}, // degenerate arc
                       CircularArcIntersector::ONE_POINT_INTERSECTION,
-                      CoordinateXY{2, 2});
+                      XY{2, 2});
 
-    checkIntersection({-1, -4}, {1, 0}, {3, 4}, // degenerate arc
-                      {0, 0}, {2, 2}, {4, 0}, // CW arc
+    checkIntersection(XY{-1, -4}, XY{1, 0}, XY{3, 4}, // degenerate arc
+                      XY{0, 0}, XY{2, 2}, XY{4, 0}, // CW arc
                       CircularArcIntersector::ONE_POINT_INTERSECTION,
-                      CoordinateXY{2, 2});
+                      XY{2, 2});
 }
 
 template<>
@@ -413,10 +585,10 @@ void object::test<21>()
 {
     set_test_name("two degenerate arcs with single interior intersection");
 
-    checkIntersection({0, 0}, {4, 4}, {10, 10},
-                      {10, 0}, {1, 9}, {0, 10},
+    checkIntersection(XY{0, 0}, XY{4, 4}, XY{10, 10},
+                      XY{10, 0}, XY{1, 9}, XY{0, 10},
                       CircularArcIntersector::ONE_POINT_INTERSECTION,
-                      CoordinateXY{5, 5});
+                      XY{5, 5});
 }
 
 template<>
@@ -425,10 +597,12 @@ void object::test<30>()
 {
     set_test_name("arc-segment with single interior intersection");
 
-    checkIntersection({0, 0}, {2, 2}, {4, 0},
-    {1, 0}, {3, 4},
-    CircularArcIntersector::ONE_POINT_INTERSECTION,
-    {2, 2});
+    checkIntersectionArcSeg(
+        XY{0, 0}, XY{2, 2}, XY{4, 0},
+        XY{1, 0}, XY{3, 4},
+        CircularArcIntersector::ONE_POINT_INTERSECTION,
+        XY{2, 2}
+    );
 }
 
 template<>
@@ -437,10 +611,12 @@ void object::test<31>()
 {
     set_test_name("arc-vertical segment with single interior intersection");
 
-    checkIntersection({-2, 0}, {0, 2}, {2, 0},
-    {0, 0}, {0, 4},
-    CircularArcIntersector::ONE_POINT_INTERSECTION,
-    {0, 2});
+    checkIntersectionArcSeg(
+        XY{-2, 0}, XY{0, 2}, XY{2, 0},
+        XY{0, 0}, XY{0, 4},
+        CircularArcIntersector::ONE_POINT_INTERSECTION,
+        XY{0, 2}
+    );
 }
 
 template<>
@@ -449,10 +625,12 @@ void object::test<32>()
 {
     set_test_name("arc-segment with two interior intersections");
 
-    checkIntersection(_W, _E, _SW,
-    {-10, 10}, {10, -10},
-    CircularArcIntersector::TWO_POINT_INTERSECTION,
-    _NW, _SE);
+    checkIntersectionArcSeg(
+        W_, E_, SW_,
+        XY{-10, 10}, XY{10, -10},
+        CircularArcIntersector::TWO_POINT_INTERSECTION,
+        NW_, SE_
+    );
 }
 
 template<>
@@ -461,10 +639,12 @@ void object::test<33>()
 {
     set_test_name("arc-vertical segment with two interior intersections");
 
-    checkIntersection(_W, _E, _SW,
-    {0, -2}, {0, 2},
-    CircularArcIntersector::TWO_POINT_INTERSECTION,
-    _S, _N);
+    checkIntersectionArcSeg(
+        W_, E_, SW_,
+        XY{0, -2}, XY{0, 2},
+        CircularArcIntersector::TWO_POINT_INTERSECTION,
+        S_, N_
+    );
 }
 
 template<>
@@ -473,9 +653,11 @@ void object::test<34>()
 {
     set_test_name("arc-segment disjoint with bbox containment");
 
-    checkIntersection(_W, _N, _E,
-    {0, 0}, {0.2, 0.2},
-    CircularArcIntersector::NO_INTERSECTION);
+    checkIntersectionArcSeg(
+        W_, N_, E_,
+        XY{0, 0}, XY{0.2, 0.2},
+        CircularArcIntersector::NO_INTERSECTION
+    );
 }
 
 template<>
@@ -484,10 +666,12 @@ void object::test<35>()
 {
     set_test_name("degenerate arc-segment with interior intersection");
 
-    checkIntersection({-5, -5}, {0, 0}, {5, 5},
-                      {-5, 5},  {5, -5},
-                      CircularArcIntersector::ONE_POINT_INTERSECTION,
-                      {0, 0});
+    checkIntersectionArcSeg(
+        XY{-5, -5}, XY{0, 0}, XY{5, 5},
+        XY{-5, 5},  XY{5, -5},
+        CircularArcIntersector::ONE_POINT_INTERSECTION,
+        XY{0, 0}
+    );
 }
 
 template<>
@@ -496,10 +680,12 @@ void object::test<36>()
 {
     set_test_name("intersection between a segment and a degenerate arc (radius = Infinity)");
 
-    checkIntersection({-5, -5}, {0, 0}, {5, 5 + 1e-14},
-                      {-5, 5}, {5, -5},
-                      CircularArcIntersector::ONE_POINT_INTERSECTION,
-                      CoordinateXY{0, 0});
+    checkIntersectionArcSeg(
+        XY{-5, -5}, XY{0, 0}, XY{5, 5 + 1e-14},
+        XY{-5, 5}, XY{5, -5},
+        CircularArcIntersector::ONE_POINT_INTERSECTION,
+        XY{0, 0}
+    );
 }
 
 template<>
@@ -508,10 +694,12 @@ void object::test<37>()
 {
     set_test_name("intersection between a segment and a nearly-degenerate arc (radius ~= 1e5)");
 
-    checkIntersection({-5, -5}, {0, 0}, {5, 5 + 1e-4},
-                      {-5, 5}, {5, -5},
-                      CircularArcIntersector::ONE_POINT_INTERSECTION,
-                      CoordinateXY{0, 0});
+    checkIntersectionArcSeg(
+        XY{-5, -5}, XY{0, 0}, XY{5, 5 + 1e-4},
+        XY{-5, 5}, XY{5, -5},
+        CircularArcIntersector::ONE_POINT_INTERSECTION,
+        XY{0, 0}
+    );
 }
 
 template<>
@@ -522,62 +710,62 @@ void object::test<38>()
     // https://github.com/claeis/iox-ili/blob/master/jtsext/src/test/java/ch/interlis/iom_j/itf/impl/hrg/ISCILRTest.java
 
     // test_1a
-    checkIntersection({0, 5}, {5, 0}, {0, -5},
-                      {20, 5}, {20, -5},
-                      CircularArcIntersector::NO_INTERSECTION),
+    checkIntersectionArcSeg(XY{0, 5},  XY{5, 0}, XY{0, -5},
+                          XY{20, 5}, XY{20, -5},
+                          CircularArcIntersector::NO_INTERSECTION),
 
     // test_2a
-    checkIntersection({0, 5}, {5, 0}, {0, -5},
-                      {5, 5}, {5, 0},
+    checkIntersectionArcSeg(XY{0, 5}, XY{5, 0}, XY{0, -5},
+                      XY{5, 5}, XY{5, 0},
                       CircularArcIntersector::ONE_POINT_INTERSECTION,
-                      {5, 0});
+                      XY{5, 0});
 
     // test_2b
-    checkIntersection({0, 5}, {5, 0}, {0, -5},
-                      {5, 5}, {5, -5},
+    checkIntersectionArcSeg(XY{0, 5}, XY{5, 0}, XY{0, -5},
+                      XY{5, 5}, XY{5, -5},
                       CircularArcIntersector::ONE_POINT_INTERSECTION,
-                      {5, 0});
+                      XY{5, 0});
 
     // test_2c
-    checkIntersection({0, 5}, {4, 3}, {0, -5},
-                      {5, 5}, {5, 0},
+    checkIntersectionArcSeg(XY{0, 5}, XY{4, 3}, XY{0, -5},
+                      XY{5, 5}, XY{5, 0},
                       CircularArcIntersector::ONE_POINT_INTERSECTION,
-                      {5, 0});
+                      XY{5, 0});
 
     // test_2d
-    checkIntersection({0, 5}, {4, 3}, {0, -5},
-                      {5, 5}, {5, -5},
+    checkIntersectionArcSeg(XY{0, 5}, XY{4, 3}, XY{0, -5},
+                      XY{5, 5}, XY{5, -5},
                       CircularArcIntersector::ONE_POINT_INTERSECTION,
-                      {5, 0});
+                      XY{5, 0});
 
     // test_3a
-    checkIntersection({0, 5}, {5, 0}, {0, -5},
-                      {4, 5}, {4, -5},
+    checkIntersectionArcSeg(XY{0, 5}, XY{5, 0}, XY{0, -5},
+                      XY{4, 5}, XY{4, -5},
                       CircularArcIntersector::TWO_POINT_INTERSECTION,
-                      {4, 3}, {4, -3});
+                      XY{4, 3}, XY{4, -3});
 
     // test_3b
-    checkIntersection({0, 5}, {5, 0}, {0, -5},
-                      {-4, 5}, {-4, -5},
+    checkIntersectionArcSeg(XY{0, 5},  XY{5, 0}, XY{0, -5},
+                      XY{-4, 5}, XY{-4, -5},
                       CircularArcIntersector::NO_INTERSECTION);
 
     // test_3c
-    checkIntersection({0, 5}, {5, 0}, {0, -5},
-                      {4, 10}, {4, 5},
+    checkIntersectionArcSeg(XY{0, 5},  XY{5, 0}, XY{0, -5},
+                      XY{4, 10}, XY{4, 5},
                       CircularArcIntersector::NO_INTERSECTION);
 
 
     // test_3d
-    checkIntersection({0, 5}, {3, 4}, {5, 0},
-                      {4, 5}, {4, -5},
+    checkIntersectionArcSeg(XY{0, 5}, XY{3, 4}, XY{5, 0},
+                      XY{4, 5}, XY{4, -5},
                       CircularArcIntersector::ONE_POINT_INTERSECTION,
-                      {4, 3});
+                      XY{4, 3});
 
     // test_3e
-    checkIntersection({0, 5}, {5, 0}, {0, -5},
-                      {4, 5}, {4, 0},
+    checkIntersectionArcSeg(XY{0, 5}, XY{5, 0}, XY{0, -5},
+                      XY{4, 5}, XY{4, 0},
                       CircularArcIntersector::ONE_POINT_INTERSECTION,
-                      {4, 3});
+                      XY{4, 3});
 }
 
 template<>
@@ -588,54 +776,54 @@ void object::test<39>()
     // https://github.com/claeis/iox-ili/blob/master/jtsext/src/test/java/ch/interlis/iom_j/itf/impl/hrg/ISCICRTest.java
 
     // test_1: circles do not overlap
-    checkIntersection({0, 5}, {5, 0}, {0, -5},
-                      {20, 5}, {15, 0}, {20, -5},
+    checkIntersection(XY{0, 5},  XY{5, 0},  XY{0, -5},
+                      XY{20, 5}, XY{15, 0}, XY{20, -5},
                       CircularArcIntersector::NO_INTERSECTION);
 
     // test_2a: arcs overlap at a point
-    checkIntersection({0, 5}, {5, 0}, {0, -5},
-                      {10, 5}, {5, 0}, {10, -5},
+    checkIntersection(XY{0, 5},  XY{5, 0}, XY{0, -5},
+                      XY{10, 5}, XY{5, 0}, XY{10, -5},
                       CircularArcIntersector::ONE_POINT_INTERSECTION,
-                      CoordinateXY{5, 0});
+                      XY{5, 0});
     // test_2b: arcs overlap at a point that is not a definition point of either arc
-    checkIntersection({0, 5}, {4, 3}, {0, -5},
-                      {10, 5}, {6, 3}, {10, -5},
+    checkIntersection(XY{0, 5},  XY{4, 3}, XY{0, -5},
+                      XY{10, 5}, XY{6, 3}, XY{10, -5},
                       CircularArcIntersector::ONE_POINT_INTERSECTION,
-                      CoordinateXY{5, 0});
+                      XY{5, 0});
 
     // test_3a: circles overlap at two points that are within both arcs
-    checkIntersection({0, 5}, {5, 0}, {0, -5},
-                      {8, 5}, {3, 0}, {8, -5},
+    checkIntersection(XY{0, 5}, XY{5, 0}, XY{0, -5},
+                      XY{8, 5}, XY{3, 0}, XY{8, -5},
                       CircularArcIntersector::TWO_POINT_INTERSECTION,
-                      CoordinateXY{4, 3}, CoordinateXY{4, -3});
+                      XY{4, 3}, XY{4, -3});
 
     // test_3b: circles overlap at two points but neither is on the first arc
-    checkIntersection({0, 5}, {-5, 0}, {0, -5},
-                      {8, 5}, {3, 0}, {8, -5},
+    checkIntersection(XY{0, 5}, XY{-5, 0}, XY{0, -5},
+                      XY{8, 5}, XY{3, 0},  XY{8, -5},
                       CircularArcIntersector::NO_INTERSECTION);
 
     // test_3c: circles overlap at two points but neither is on the first or second arc
-    checkIntersection({0, 5}, {-5, 0}, {0, -5},
-                      {8, 5}, {13, 0}, {8, -5},
+    checkIntersection(XY{0, 5}, XY{-5, 0}, XY{0, -5},
+                      XY{8, 5}, XY{13, 0}, XY{8, -5},
                       CircularArcIntersector::NO_INTERSECTION);
 
     // test_3d: circles overlap at two points but one is not on the first arc
-    checkIntersection({5, 0}, {3, -4}, {0, -5},
-                      {8, 5}, {3, 0}, {8, -5},
+    checkIntersection(XY{5, 0}, XY{3, -4}, XY{0, -5},
+                      XY{8, 5}, XY{3, 0},  XY{8, -5},
                       CircularArcIntersector::ONE_POINT_INTERSECTION,
-                      CoordinateXY{4, -3});
+                      XY{4, -3});
 
     // test_3e: circles overlap at two points but one is not on the second arc
-    checkIntersection({0, 5}, {5, 0}, {0, -5},
-                      {3, 0}, {5, -4}, {8, -5},
+    checkIntersection(XY{0, 5}, XY{5, 0},  XY{0, -5},
+                      XY{3, 0}, XY{5, -4}, XY{8, -5},
                       CircularArcIntersector::ONE_POINT_INTERSECTION,
-                      CoordinateXY{4, -3});
+                      XY{4, -3});
 
     // test_4a: cocircular
-    checkIntersection({0, 5}, {5, 0}, {0, -5},
-                      {4, 3}, {5, 0}, {4, -3},
+    checkIntersection(XY{0, 5}, XY{5, 0}, XY{0, -5},
+                      XY{4, 3}, XY{5, 0}, XY{4, -3},
                       CircularArcIntersector::COCIRCULAR_INTERSECTION,
-                      CircularArc{{4, 3}, {5, 0}, {4, -3}});
+                      CircularArc::create(XY{4, 3}, {5, 0}, {4, -3}));
 }
 
 #if 0
@@ -646,10 +834,10 @@ void object::test<40>()
 {
     set_test_name("intersection between a segment and a nearly-degenerate arc (radius ~= 2e6)");
 
-    checkIntersection({-5, -5}, {0, 0}, {5, 5 + 1e-9},
-                      {-5, 5}, {5, -5},
+    checkIntersection(XY{-5, -5}, XY{0, 0}, XY{5, 5 + 1e-9},
+                      XY{-5, 5},  XY{5, -5},
                       CircularArcIntersector::ONE_POINT_INTERSECTION,
-                      CoordinateXY{0, 0});
+                      XY{0, 0});
 }
 #endif
 
@@ -659,13 +847,13 @@ void object::test<41>()
 {
     set_test_name("IOX-ILI: testFastGerade");
 
-    checkIntersection({611770.424, 234251.322}, {611770.171, 234250.059}, {611769.918, 234248.796},
-    {611613.84, 233467.819},
-    {611610.392, 233468.995},
-    CircularArcIntersector::NO_INTERSECTION);
+    checkIntersectionArcSeg(
+        XY{611770.424, 234251.322}, XY{611770.171, 234250.059}, XY{611769.918, 234248.796},
+        XY{611613.84, 233467.819},  XY{611610.392, 233468.995},
+        CircularArcIntersector::NO_INTERSECTION
+    );
 }
 
-#if 0
 template<>
 template<>
 void object::test<42>()
@@ -674,87 +862,91 @@ void object::test<42>()
     // two nearly-linear arcs touching at a single endpoint
     // Potential fix is to use tolerance for checking if computed points are within arc.
 
-    checkIntersection({645175.553, 248745.374}, { 645092.332, 248711.677}, { 645009.11, 248677.98},
-                      {645009.11, 248677.98}, {644926.69, 248644.616}, { 644844.269, 248611.253},
-                       CircularArcIntersector::ONE_POINT_INTERSECTION,
-                      CoordinateXY{645009.110, 248677.980});
+    checkIntersection(
+        XY{645175.553, 248745.374}, XY{ 645092.332, 248711.677}, XY{ 645009.11, 248677.98},
+        XY{645009.11, 248677.98}, XY{644926.69, 248644.616}, XY{ 644844.269, 248611.253},
+        CircularArcIntersector::ONE_POINT_INTERSECTION,
+        XY{645009.110, 248677.980}
+    );
 }
-#endif
 
 template<>
 template<>
 void object::test<43>()
 {
-    set_test_name("IOX-ILI: overlayTwoARCS_SameEndPoints_SameDirection");
+    set_test_name("IOX-ILI: overlayTwoARCS_SameEndPointsS_ameDirection");
     // two arcs with same arcPoint and radius.
     // startPoints and endPoints are same. lines are in same direction
 
     checkIntersection(
-    {100.0, 100.0},{120,150.0},{100.0,200.0},
-    {100.0, 100.0},{120,150.0},{100.0,200.0},
-    CircularArcIntersector::COCIRCULAR_INTERSECTION,
-    CircularArc{{100.0, 100.0}, {120, 150}, {100, 200}});
+        XY{100.0, 100.0}, XY{120,150.0}, XY{100.0,200.0},
+        XY{100.0, 100.0}, XY{120,150.0}, XY{100.0,200.0},
+        CircularArcIntersector::COCIRCULAR_INTERSECTION,
+        CircularArc::create(XY{100.0, 100.0}, {120, 150}, {100, 200})
+    );
 }
 
 template<>
 template<>
 void object::test<44>()
 {
-    set_test_name("IOX-ILI: overlayTwoARCS_DifferentArcPointOnSameArcLine_SameDirection");
-	// two arcs with different arcPoint (on same arcLine) and same radius length.
-	// startPoints and endPoints are same. lines are in same direction.
+    set_test_name("IOX-ILI: overlayTwoARCS_DifferentArcPointOnSameArcLineS_ameDirection");
+    // two arcs with different arcPoint (on same arcLine) and same radius length.
+    // startPoints and endPoints are same. lines are in same direction.
 
     checkIntersection(
-    {0.0, 10.0},{4.0,8.0},{0.0,0.0},
-    {0.0, 10.0},{4.0,2.0},{0.0,0.0},
-    CircularArcIntersector::COCIRCULAR_INTERSECTION,
-    CircularArc{{0, 10}, {5, 5}, {0, 0}});
+        XY{0.0, 10.0}, XY{4.0,8.0}, XY{0.0,0.0},
+        XY{0.0, 10.0}, XY{4.0,2.0}, XY{0.0,0.0},
+        CircularArcIntersector::COCIRCULAR_INTERSECTION,
+        CircularArc::create(XY{0, 10}, {5, 5}, {0, 0})
+    );
 }
 
 template<>
 template<>
 void object::test<45>()
 {
-    set_test_name("IOX-ILI: overlayTwoARCS_SameArcPointOnSameArcLine_OneArcLineIsLonger");
+    set_test_name("IOX-ILI: overlayTwoARCSS_ameArcPointOnSameArcLine_OneArcLineIsLonger");
     // two arcs with same arcPoint (on same arcLine) and same radius length.
     // one arc line is longer than the other arc line.
     // startPoints is same, endPoints are different. lines are in same direction.
 
     checkIntersection(
-    {0.0, 10.0},{4.0,8.0},{0.0,0.0},
-    {0.0, 10.0},{4.0,8.0},{4.0,2.0},
-    CircularArcIntersector::COCIRCULAR_INTERSECTION,
-    CircularArc{{0, 10}, {4, 2}, {0, 5}, 5, Orientation::CLOCKWISE});
+        XY{0.0, 10.0}, XY{4.0,8.0}, XY{0.0,0.0},
+        XY{0.0, 10.0}, XY{4.0,8.0}, XY{4.0,2.0},
+        CircularArcIntersector::COCIRCULAR_INTERSECTION,
+        CircularArc::create(XY{0, 10}, XY{4, 2}, XY{0, 5}, 5, Orientation::CLOCKWISE)
+    );
 }
 
 template<>
 template<>
 void object::test<46>() 
 {
-    set_test_name("IOX-ILI: overlayTwoARCS_SameEndPoints_OtherDirection");
+    set_test_name("IOX-ILI: overlayTwoARCSS_ameEndPoints_OtherDirection");
     // two arcs with same arcPoint and radius
     // startPoint1 is equal to endPoint2, startPoint2 is equal to endPoint1.
 
     checkIntersection(
-        CircularArc({100.0, 100.0}, {80.0, 150.0}, {100.0, 200.0}),
-        CircularArc({100.0, 200.0}, {80.0, 150.0}, {100.0, 100.0}),
+        XY{100.0, 100.0}, XY{80.0, 150.0}, XY{100.0, 200.0},
+        XY{100.0, 200.0}, XY{80.0, 150.0}, XY{100.0, 100.0},
         CircularArcIntersector::COCIRCULAR_INTERSECTION,
-        CircularArc({100.0, 100.0}, {80.0, 150.0}, {100.0, 200.0}));
+        CircularArc::create(XY{100.0, 100.0}, {80.0, 150.0}, {100.0, 200.0}));
 }
 
 template<>
 template<>
 void object::test<47>() 
 {
-    set_test_name("IOX-ILI: overlayTwoARCS_DifferentStartPoints_SameDirection_DifferentLength");
+    set_test_name("IOX-ILI: overlayTwoARCS_DifferentStartPointsS_ameDirection_DifferentLength");
     // two arcs. ArcPoint is equal. different angle.
     // startPoints are different. endPoints are same.
-    CircularArc a({70.0, 60.0}, {50.0, 100.0}, {60.0, 130.0});
-    CircularArc b({60.0, 70.0}, {50.0, 100.0}, {60.0, 130.0});
+    CircularArc a = CircularArc::create(XY{70.0, 60.0}, {50.0, 100.0}, {60.0, 130.0});
+    CircularArc b = CircularArc::create(XY{60.0, 70.0}, {50.0, 100.0}, {60.0, 130.0});
 
     checkIntersection(a, b,
         CircularArcIntersector::COCIRCULAR_INTERSECTION,
-        CircularArc({60, 70}, {60, 130}, a.getCenter(), a.getRadius(), a.getOrientation()));
+        CircularArc::create(XY{60, 70}, {60, 130}, a.getCenter(), a.getRadius(), a.getOrientation()));
 }
 
 template<>
@@ -766,12 +958,12 @@ void object::test<48>()
     // ArcPoint is equal.
     // startPoints are different. endPoints are different.
 
-    CircularArc a({70.0,  60.0}, {50.0, 100.0}, {70.0, 140.0});
-    CircularArc b({60.0, 130.0}, {50.0, 100.0}, {60.0,  70.0});
+    CircularArc a = CircularArc::create(XY{70.0,  60.0}, {50.0, 100.0}, {70.0, 140.0});
+    CircularArc b = CircularArc::create(XY{60.0, 130.0}, {50.0, 100.0}, {60.0,  70.0});
 
     checkIntersection(a, b, 
             CircularArcIntersector::COCIRCULAR_INTERSECTION,
-            CircularArc({60, 70}, {60, 130}, a.getCenter(), a.getRadius(), a.getOrientation()));
+            CircularArc::create(XY{60, 70}, XY{60, 130}, a.getCenter(), a.getRadius(), a.getOrientation()));
 
 }
 
@@ -779,13 +971,13 @@ template<>
 template<>
 void object::test<49>()
 {
-    set_test_name("IOX-ILI: overlayTwoARCS_DifferentEndPoints_SameDirection_DifferentLength");
+    set_test_name("IOX-ILI: overlayTwoARCS_DifferentEndPointsS_ameDirection_DifferentLength");
     // Two arcs with same orientation. 
     // ArcPoint is equal.
     // startPoints are same, endpoints are different
 
-    CircularArc a({70.0, 60.0}, {50.0, 100.0}, {70.0, 140.0});
-    CircularArc b({70.0, 60.0}, {50.0, 100.0}, {60.0, 130.0});
+    CircularArc a = CircularArc::create(XY{70.0, 60.0}, {50.0, 100.0}, {70.0, 140.0});
+    CircularArc b = CircularArc::create(XY{70.0, 60.0}, {50.0, 100.0}, {60.0, 130.0});
 
     checkIntersection(a, b,
         CircularArcIntersector::COCIRCULAR_INTERSECTION, b);
@@ -801,23 +993,23 @@ void object::test<50>()
     // One endpoint is the same, one is different.
 
 
-    CircularArc a({70.0,  60.0}, {50.0, 100.0}, {70.0, 140.0});
-    CircularArc b({60.0, 130.0}, {50.0, 100.0}, {70.0,  60.0});
+    CircularArc a = CircularArc::create(XY{70.0,  60.0}, {50.0, 100.0}, {70.0, 140.0});
+    CircularArc b = CircularArc::create(XY{60.0, 130.0}, {50.0, 100.0}, {70.0,  60.0});
 
     checkIntersection(a, b,
         CircularArcIntersector::COCIRCULAR_INTERSECTION,
-        CircularArc({70, 60}, {60, 130}, a.getCenter(), a.getRadius(), a.getOrientation()));
+        CircularArc::create(XY{70, 60}, XY{60, 130}, a.getCenter(), a.getRadius(), a.getOrientation()));
 }
 
 template<>
 template<>
 void object::test<51>()
 {
-    set_test_name("IOX-ILI: twoARCS_SameRadiusAndCenter_DontOverlay");
+    set_test_name("IOX-ILI: twoARCSS_ameRadiusAndCenter_DontOverlay");
     // two arcs with same center and radius that don't touch each other.
 
-    CircularArc a({70.0,  60.0}, {50.0,  100.0}, {70.0,  140.0});
-    CircularArc b({140.0, 70.0}, {150.0, 100.0}, {140.0, 130.0});
+    CircularArc a = CircularArc::create(XY{70.0,  60.0}, {50.0,  100.0}, {70.0,  140.0});
+    CircularArc b = CircularArc::create(XY{140.0, 70.0}, {150.0, 100.0}, {140.0, 130.0});
 
     checkIntersection(a, b, CircularArcIntersector::NO_INTERSECTION);
 }
@@ -826,30 +1018,28 @@ template<>
 template<>
 void object::test<52>()
 {
-    set_test_name("IOX-ILI: twoARCS_SameRadiusAndCenter_Touch_DontOverlay");
+    set_test_name("IOX-ILI: twoARCSS_ameRadiusAndCenter_Touch_DontOverlay");
     // Two arcs with same radius and center that touch at the endpoints
 
-    CircularArc a({50.0,  100.0}, {100.0, 150.0}, {150.0, 100.0});
-    CircularArc b({150.0, 100.0}, {100.0, 50.0},  {50.0,  100.0});
+    CircularArc a = CircularArc::create(XY{50.0,  100.0}, {100.0, 150.0}, {150.0, 100.0});
+    CircularArc b = CircularArc::create(XY{150.0, 100.0}, {100.0, 50.0},  {50.0,  100.0});
 
-    checkIntersection(a, b, CircularArcIntersector::TWO_POINT_INTERSECTION, a.p0, a.p2);
+    checkIntersection(a, b, CircularArcIntersector::TWO_POINT_INTERSECTION, a.p0(), a.p2());
 }
 
-#if 0
 template<>
 template<>
 void object::test<53>()
 {
-    set_test_name("IOX-ILI: twoARCS_SameRadiusAndCenter_Touch_DontOverlay_real");
+    set_test_name("IOX-ILI: twoARCSS_ameRadiusAndCenter_Touch_DontOverlay_real");
     // arcs touch at endpoints
     // Potential fix is to use tolerance for checking if computed points are within arc.
 
-    CircularArc a({2654828.912, 1223354.671}, {2654829.982, 1223353.601}, {2654831.052, 1223354.671});
-    CircularArc b({2654831.052, 1223354.671}, {2654829.982, 1223355.741}, {2654828.912, 1223354.671});
+    CircularArc a = CircularArc::create(XY{2654828.912, 1223354.671}, {2654829.982, 1223353.601}, {2654831.052, 1223354.671});
+    CircularArc b = CircularArc::create(XY{2654831.052, 1223354.671}, {2654829.982, 1223355.741}, {2654828.912, 1223354.671});
 
-    checkIntersection(a, b, CircularArcIntersector::TWO_POINT_INTERSECTION, a.p0, a.p2);
+    checkIntersection(a, b, CircularArcIntersector::TWO_POINT_INTERSECTION, a.p0(), a.p2());
 }
-#endif
 
 template<>
 template<>
@@ -858,13 +1048,16 @@ void object::test<54>()
     set_test_name("IOX-ILI: twoARCS_intersect0");
     // https://github.com/claeis/ilivalidator/issues/186
 
-    CircularArc a({2658317.225, 1250832.586}, {2658262.543, 1250774.465}, {2658210.528, 1250713.944});
-    CircularArc b({2658211.456, 1250715.072}, {2658161.386, 1250651.279}, {2658114.283, 1250585.266});
+    CircularArc a = CircularArc::create(XY{2658317.225, 1250832.586}, {2658262.543, 1250774.465}, {2658210.528, 1250713.944});
+    CircularArc b = CircularArc::create(XY{2658211.456, 1250715.072}, {2658161.386, 1250651.279}, {2658114.283, 1250585.266});
 
     // An intersection is visually apparent in QGIS, but CGAL 5.6 reports no intersections...
     checkIntersection(a, b, CircularArcIntersector::NO_INTERSECTION);
 }
 
+#if 0
+// Failing because two intersection points are detected.
+// One is and endpoint (exact), the other is an approximation of the same endpoint.
 template<>
 template<>
 void object::test<55>()
@@ -878,7 +1071,548 @@ void object::test<55>()
     // expected result calculated with CGAL 5.6
     checkIntersection(a, b,
         CircularArcIntersector::ONE_POINT_INTERSECTION,
-        CoordinateXY{2653134.35399999982, 1227788.18800000008});
+        XY{2653134.35399999982, 1227788.18800000008});
+}
+#endif
+
+/// Z/M interpolation
+
+template<>
+template<>
+void object::test<56>()
+{
+    set_test_name("arc XYZ / arc XYZ interior intersection");
+    // Z value at the intersection point is the average of the interpolated values from each arc
+    // In both cases the value is interpolated from points 2 3 of the arc
+
+    checkIntersection(
+        XYZ{-5, 0, 1}, XYZ{-4, 3, 41}, XYZ{4, 3, 53},
+        XYZ{-5, 10, 0}, XYZ{-2, 9, 7}, XYZ{-2, 1, 13},
+        CircularArcIntersector::ONE_POINT_INTERSECTION,
+        XYZ{0, 5, 0.25*(41 + 53 + 7 + 13)}
+    );
 }
 
-}
\ No newline at end of file
+template<>
+template<>
+void object::test<57>()
+{
+    set_test_name("arc XYZ / segment XYZ interior intersection");
+    // Z value at the intersection point is the average of the interpolated values from the arc and segment
+
+    checkIntersectionArcSeg(
+        XYZ{-5, 0, 1}, XYZ{-4, 3, 41}, XYZ{4, 3, 53},
+        XYZ{0, 0, 7}, XYZ{0, 10, 13},
+        CircularArcIntersector::ONE_POINT_INTERSECTION,
+        XYZ{0, 5, 0.25*(41 + 53 + 7 + 13)}
+    );
+}
+
+template<>
+template<>
+void object::test<58>()
+{
+    set_test_name("arc XYZ / arc XYM interior intersection");
+    // Z value at the intersection point is interpolated from the arc with Z values
+    // M value at the intersection point is interpolated from the arc with M values
+
+    checkIntersection(
+        XYZ{-5, 0, 1}, XYZ{-4, 3, 41}, XYZ{4, 3, 53},
+        XYM{-5, 10, 0}, XYM{-2, 9, 7}, XYM{-2, 1, 13},
+        CircularArcIntersector::ONE_POINT_INTERSECTION,
+        XYZM{0, 5, 0.5*(41 + 53), 0.5*(7 + 13)}
+    );
+}
+
+template<>
+template<>
+void object::test<59>()
+{
+    set_test_name("arc XYZ / arc XYZ interior intersection with control point Z = NaN");
+    // Z value at the intersection point is the average of the interpolated values from each arc
+    // Because the control point Z is NaN, interpolation is done from the arc endpoints
+
+    checkIntersection(
+        XYZ{-5, 0, 41}, XYZ{-4, 3, NaN}, XYZ{5, 0, 53},
+        XYZ{-2, 9, 7}, XYZ{-1, 8, NaN}, XYZ{-2, 1, 13},
+        CircularArcIntersector::ONE_POINT_INTERSECTION,
+        XYZ{0, 5, 0.25*(41 + 53 + 7 + 13)}
+    );
+}
+
+template<>
+template<>
+void object::test<60>()
+{
+    set_test_name("arc XYM / arc XYZ interior intersection with control point Z/M = NaN");
+    // Z value at the intersection point is interpolated from the arc with Z values
+    // Because the control point Z is NaN, interpolation is done from the arc endpoints
+    // M value at the intersection point is interpolated from the arc with M values
+    // Because the control point M is NaN, interpolation is done from the arc endpoints
+
+    checkIntersection(
+        XYM{-5, 0, 41}, XYM{-4, 3, NaN}, XYM{5, 0, 53},
+        XYZ{-2, 9, 7}, XYZ{-1, 8, NaN}, XYZ{-2, 1, 13},
+        CircularArcIntersector::ONE_POINT_INTERSECTION,
+        XYZM{0, 5, 0.5*(7 + 13), 0.5*(41 + 53)}
+    );
+}
+
+template<>
+template<>
+void object::test<61>()
+{
+    set_test_name("arc XYM / arc XYZ interior intersection with endpoint Z/M = NaN");
+    // Z value at the intersection point is interpolated from the arc with Z values
+    // Because the endpoint Z is NaN, the value of the control point is taken
+    // M value at the intersection point is interpolated from the arc with M values
+    // Because the endpoint M is NaN, the value of the control point is taken
+
+    checkIntersection(
+        XYM{-5, 0, 41}, XYM{-4, 3, 53}, XYM{5, 0, NaN},
+        XYZ{-2, 9, 7}, XYZ{-1, 8, 13}, XYZ{-2, 1, NaN},
+        CircularArcIntersector::ONE_POINT_INTERSECTION,
+        XYZM{0, 5, 13, 53}
+    );
+}
+
+template<>
+template<>
+void object::test<62>()
+{
+    set_test_name("arc XYM / arc XYZ interior intersection with control point and endpoint Z/M = NaN");
+    // Z value at the intersection point is interpolated from the arc with Z values
+    // Because the endpoint and control point Z is NaN, the value of the other endpoint is taken
+    // M value at the intersection point is interpolated from the arc with M values
+    // Because the endpoint and control point M is NaN, the value of the other endpoint is taken
+
+    checkIntersection(
+        XYM{-5, 0, 41}, XYM{-4, 3, NaN}, XYM{5, 0, NaN},
+        XYZ{-2, 9, 7}, XYZ{-1, 8, NaN}, XYZ{-2, 1, NaN},
+        CircularArcIntersector::ONE_POINT_INTERSECTION,
+        XYZM{0, 5, 7, 41}
+    );
+}
+
+template<>
+template<>
+void object::test<63>()
+{
+    set_test_name("arc XYZ / segment XYZ interior intersection with control point Z = NaN");
+    // Z value at the intersection point is the average of the interpolated values from the arc and the segment
+    // Because the control point Z is NaN, interpolation is done from the arc endpoints
+
+    checkIntersectionArcSeg(
+        XYZ{-5, 0, 41}, XYZ{-4, 3, NaN}, XYZ{5, 0, 53},
+        XYZ{0, 0, 7}, XYZ{0, 10, 13},
+        CircularArcIntersector::ONE_POINT_INTERSECTION,
+        XYZ{0, 5, 0.25*(41 + 53 + 7 + 13)}
+    );
+}
+
+template<>
+template<>
+void object::test<64>()
+{
+    set_test_name("arc XYZ / segment XYZ interior intersection, segment endpoint Z = NaN");
+    // Z value at the intersection point is the average of the interpolated values from the arc and the other segment endpoint
+
+    checkIntersectionArcSeg(
+        XYZ{-5, 0, 7}, XYZ{-3, 4, 41}, XYZ{3, 4, 53},
+        XYZ{0, 0, NaN}, XYZ{0, 10, 13},
+        CircularArcIntersector::ONE_POINT_INTERSECTION,
+        XYZ{0, 5, 0.25*(41 + 53 + 13 + 13)}
+    );
+}
+
+template<>
+template<>
+void object::test<65>()
+{
+    set_test_name("arc XYZ / arc XYZ endpoint intersection");
+    // Result Z value at intersection point is taken from the first input
+
+    checkIntersection(
+        XYZ{0, 0, 0}, XYZ{1, 1, 1}, XYZ{2, 0, 2},
+        XYZ{2, 0, 500}, XYZ{3, -1, 501}, XYZ{4, 0, 502},
+        CircularArcIntersector::ONE_POINT_INTERSECTION,
+        XYZ{2, 0, 2}
+    );
+}
+
+template<>
+template<>
+void object::test<66>()
+{
+    set_test_name("arc XYZ / segment XYZ endpoint intersection");
+    // Result Z value at intersection point is taken from the first input
+    // Related: RobustLineIntersectorZTest::testEndpoint
+
+    checkIntersectionArcSeg(
+        XYZ{0, 0, 0},   XYZ{1, 1, 1}, XYZ{2, 0, 2},
+        XYZ{2, 0, 500}, XYZ{4, 0, 502},
+        CircularArcIntersector::ONE_POINT_INTERSECTION,
+        XYZ{2, 0, 2}
+    );
+
+    // Same inputs as above but with order reversed
+    checkIntersectionSegArc(
+        XYZ{2, 0, 500}, XYZ{4, 0, 502},
+        XYZ{0, 0, 0},   XYZ{1, 1, 1}, XYZ{2, 0, 2},
+        CircularArcIntersector::ONE_POINT_INTERSECTION,
+        XYZ{2, 0, 500}
+    );
+}
+
+#if 0
+// Fails because computed intersection point is not exactly equal to endpoint, so special handling doesn't apply.
+template<>
+template<>
+void object::test<67>() {
+    set_test_name("arc XYZ / arc XYZ interior / endpoint intersection");
+    // Result Z is taken from the endpoint
+    // Related: RobustLineIntersectorZTest::testInteriorEndpoint
+
+    checkIntersection(
+        XYZ{-1, 0, 1}, XYZ{0, 1, 2}, XYZ{1, 0, 3},
+        XYZ{-2, 1, 7}, XYZ{-1, 2, 8}, XYZ{0, 1, 9},
+        CircularArcIntersector::ONE_POINT_INTERSECTION,
+        XYZ{0, 1, 9}
+    );
+}
+#endif
+
+template<>
+template<>
+void object::test<68>() {
+    set_test_name("arc XYZ / seg XYZ interior / endpoint intersection");
+    // Result Z is taken from the endpoint
+    // Related: RobustLineIntersectorZTest::testInteriorEndpoint
+
+    checkIntersectionArcSeg(
+        XYZ{-1, 0, 1}, XYZ{0, 1, 2}, XYZ{1, 0, 3},
+        XYZ{0, 0, 5}, {0, 1, 9},
+        CircularArcIntersector::ONE_POINT_INTERSECTION,
+        XYZ{0, 1, 9}
+    );
+
+    // Same inputs as above but with order reversed
+    checkIntersectionSegArc(
+        XYZ{0, 0, 5}, {0, 1, 9},
+        XYZ{-1, 0, 1}, XYZ{0, 1, 2}, XYZ{1, 0, 3},
+        CircularArcIntersector::ONE_POINT_INTERSECTION,
+        XYZ{0, 1, 9}
+    );
+
+}
+
+template<>
+template<>
+void object::test<69>() {
+    set_test_name("arc XYZ / arc XY interior / endpoint intersection");
+    // Intersection is at interior of XYZ arc, endpoint of XY arc
+    // Result Z is interpolated
+    // Related: RobustLineIntersectorZTest::testInteriorEndpoint3D2D
+
+    checkIntersection(
+        XYZ{-5, 0, 1}, XYZ{-4, 3, 2}, XYZ{4, 3, 3},
+        XY{-10, 5}, XY{-5, 10}, XY{0, 5},
+        CircularArcIntersector::ONE_POINT_INTERSECTION,
+        XYZ{0, 5, 2.5}
+    );
+}
+
+template<>
+template<>
+void object::test<70>() {
+    set_test_name("arc XYZ / segment XY interior / endpoint intersection");
+    // Intersection is at interior of XYZ arc, endpoint of XY segment
+    // Result Z is interpolated
+    // Related: RobustLineIntersectorZTest::testInteriorEndpoint3D2D
+
+    checkIntersectionArcSeg(
+        XYZ{-5, 0, 1}, XYZ{-4, 3, 2}, XYZ{4, 3, 3},
+        XY{0, 0}, XY{0, 5},
+        CircularArcIntersector::ONE_POINT_INTERSECTION,
+        XYZ{0, 5, 2.5}
+    );
+}
+
+#if 0
+// Fails because computed intersection point is not exactly equal to endpoint, so special handling doesn't apply.
+template<>
+template<>
+void object::test<71>() {
+    set_test_name("arc XY / arc XYZ interior / endpoint intersection");
+    // Intersection is at interior of XY arc, endpoint of XYZ arc
+    // Result Z is from 3D endpoint
+    // Related: RobustLineIntersectorZTest::testInteriorEndpoint2D3D
+
+    checkIntersection(
+        XY{-5, 0}, XY{-4, 3}, XY{4, 3},
+        XYZ{-10, 5, 8}, XYZ{-5, 10, 11}, XYZ{0, 5, 17},
+        CircularArcIntersector::ONE_POINT_INTERSECTION,
+        XYZ{0, 5, 17}
+    );
+}
+#endif
+
+template<>
+template<>
+void object::test<72>() {
+    set_test_name("arc XY / segment XYZ interior / endpoint intersection");
+    // Intersection is at interior of XY arc, endpoint of XYZ segment
+    // Result Z is from 3D endpoint
+    // Related: RobustLineIntersectorZTest::testInteriorEndpoint2D3D
+
+    checkIntersectionArcSeg(
+        XY{-5, 0}, XY{-4, 3}, XY{4, 3},
+        XYZ{0, 0, 3}, XYZ{0, 5, 17},
+        CircularArcIntersector::ONE_POINT_INTERSECTION,
+        XYZ{0, 5, 17}
+    );
+}
+
+template<>
+template<>
+void object::test<73>() {
+    set_test_name("XYZ arc intersected with itself");
+    // Related:: RobustLineIntersectorTest::testCollinearEqual
+
+    // clockwise inputs
+    checkIntersection(
+        XYZ{-5, 0, 0}, XYZ{0, 5, 0}, XYZ{5, 0, 15},
+        XYZ{-5, 0, 0}, XYZ{0, 5, 0}, XYZ{5, 0, 15},
+        CircularArcIntersector::COCIRCULAR_INTERSECTION,
+        CircularArc::create(XYZ{-5, 0, 0}, XYZ{0, 5, 0}, XYZ{5, 0, 15})
+    );
+
+    // counter-clockwise inputs
+    checkIntersection(
+        XYZ{5, 0, 15}, XYZ{0, 5, 0}, XYZ{-5, 0, 0},
+        XYZ{5, 0, 15}, XYZ{0, 5, 0}, XYZ{-5, 0, 0},
+        CircularArcIntersector::COCIRCULAR_INTERSECTION,
+        CircularArc::create(XYZ{5, 0, 15}, XYZ{0, 5, 0}, XYZ{-5, 0, 0})
+    );
+
+    // mixed-orientation inputs
+    checkIntersection(
+        XYZ{-5, 0, 0}, XYZ{0, 5, 0}, XYZ{5, 0, 15},
+        XYZ{5, 0, 15}, XYZ{0, 5, 0}, XYZ{-5, 0, 0},
+        CircularArcIntersector::COCIRCULAR_INTERSECTION,
+        CircularArc::create(XYZ{-5, 0, 0}, XYZ{0, 5, 0}, XYZ{5, 0, 15})
+    );
+}
+
+template<>
+template<>
+void object::test<74>() {
+    set_test_name("XYZ arc intersected with 2D version of same arc");
+    // Related:: RobustLineIntersectorTest::testCollinearEqual3D2D
+
+    // clockwise inputs
+    checkIntersection(
+        XYZ{-5, 0, 0}, XYZ{0, 5, 0}, XYZ{5, 0, 15},
+        XY{-5, 0}, XY{0, 5}, XY{5, 0},
+        CircularArcIntersector::COCIRCULAR_INTERSECTION,
+        CircularArc::create(XYZ{-5, 0, 0}, XYZ{0, 5, 0}, XYZ{5, 0, 15})
+    );
+
+    // counter-clockwise input
+    checkIntersection(
+        XYZ{5, 0, 15}, XYZ{0, 5, 0}, XYZ{-5, 0, 0},
+        XY{5, 0}, XY{0, 5}, XY{-5, 0},
+        CircularArcIntersector::COCIRCULAR_INTERSECTION,
+        CircularArc::create(XYZ{5, 0, 15}, XYZ{0, 5, 0}, XYZ{-5, 0, 0})
+    );
+}
+
+template<>
+template<>
+void object::test<75>() {
+    set_test_name("Two cocircular XYZ arcs with endpoint intersections");
+    // Z values of inputs are the same and are copied to output
+    // Related:: RobustLineIntersectorTest::testCollinearEndpoint
+
+    checkIntersection(
+        XYZ{-5, 0, 3}, XYZ{0, 5, 11}, XYZ{5, 0, 15},
+        XYZ{-5, 0, 3}, XYZ{0, -5, 11}, XYZ{5, 0, 15},
+        CircularArcIntersector::TWO_POINT_INTERSECTION,
+        XYZ{-5, 0, 3},
+        XYZ{5, 0, 15}
+    );
+
+    // Same arguments as above, order reversed
+    checkIntersection(
+        XYZ{-5, 0, 3}, XYZ{0, -5, 11}, XYZ{5, 0, 15},
+        XYZ{-5, 0, 3}, XYZ{0, 5, 11}, XYZ{5, 0, 15},
+        CircularArcIntersector::TWO_POINT_INTERSECTION,
+        XYZ{-5, 0, 3},
+        XYZ{5, 0, 15}
+    );
+}
+
+template<>
+template<>
+void object::test<76>() {
+    set_test_name("Cocircular XYZ and XY arcs with endpoint intersections");
+    // Z values of result is taken from the XYZ input
+    // Related:: RobustLineIntersectorTest::testCollinearEndpoint3D2D
+
+    // clockwise inputs
+    checkIntersection(
+        XYZ{-5, 0, 3}, XYZ{0, 5, 11}, XYZ{5, 0, 15},
+        XY{-5, 0}, XY{0, -5}, XY{5, 0},
+        CircularArcIntersector::TWO_POINT_INTERSECTION,
+        XYZ{-5, 0, 3},
+        XYZ{5, 0, 15}
+    );
+
+    // Same arguments as above, order reversed
+    checkIntersection(
+        XY{-5, 0}, XY{0, -5}, XY{5, 0},
+        XYZ{-5, 0, 3}, XYZ{0, 5, 11}, XYZ{5, 0, 15},
+        CircularArcIntersector::TWO_POINT_INTERSECTION,
+        XYZ{-5, 0, 3},
+        XYZ{5, 0, 15}
+    );
+
+    // counter-clockwise inputs
+    checkIntersection(
+        XYZ{5, 0, 15}, XYZ{0, 5, 11}, XYZ{-5, 0, 3},
+        XY{5, 0}, XY{0, -5}, XY{-5, 0},
+        CircularArcIntersector::TWO_POINT_INTERSECTION,
+        XYZ{-5, 0, 3},
+        XYZ{5, 0, 15}
+    );
+
+    // Same arguments as above, order reversed
+    checkIntersection(
+        XY{5, 0}, XY{0, -5}, XY{-5, 0},
+        XYZ{5, 0, 15}, XYZ{0, 5, 11}, XYZ{-5, 0, 3},
+        CircularArcIntersector::TWO_POINT_INTERSECTION,
+        XYZ{-5, 0, 3},
+        XYZ{5, 0, 15}
+    );
+}
+
+template<>
+template<>
+void object::test<77>() {
+    set_test_name("XYZ arc intersected with a subset of itself");
+    // Related:: RobustLineIntersectorTest::testCollinearContained
+
+    const double theta = std::atan(3.0 / 4.0);
+    const double frac = theta / MATH_PI;
+
+    // Clockwise inputs
+    checkIntersection(
+        XYZ{-5, 0, 0}, XYZ{0, 5, 7.5}, XYZ{5, 0, 15},
+        XYZ{-4, 3, frac*15}, XYZ{0, 5, 7.5}, XYZ{4, 3, (1-frac)*15},
+           CircularArcIntersector::COCIRCULAR_INTERSECTION,
+                CircularArc::create(XYZ{-4, 3, frac*15}, XYZ{0, 5, 0}, XYZ{4, 3, (1-frac)*15})
+    );
+
+    // Counter-clockwise inputs
+    checkIntersection(
+        XYZ{5, 0, 15}, XYZ{0, 5, 7.5}, XYZ{-5, 0, 0},
+        XYZ{4, 3, (1-frac)*15}, XYZ{0, 5, 7.5}, XYZ{-4, 3, frac*15},
+           CircularArcIntersector::COCIRCULAR_INTERSECTION,
+                CircularArc::create(XYZ{4, 3, (1-frac)*15}, XYZ{0, 5, 0}, XYZ{-4, 3, frac*15})
+    );
+}
+
+template<>
+template<>
+void object::test<78>() {
+    set_test_name("XYZ arc intersected with a 2D subset of itself");
+    // Related:: RobustLineIntersectorTest::testCollinearContained3D2D
+
+    const double theta = std::atan(3.0 / 4.0);
+    const double frac = theta / MATH_PI;
+
+    // clockwise inputs
+    checkIntersection(
+        XYZ{-5, 0, 0}, XYZ{0, 5, 7.5}, XYZ{5, 0, 15},
+        XY{-4, 3}, XY{0, 5}, XY{4, 3},
+           CircularArcIntersector::COCIRCULAR_INTERSECTION,
+                CircularArc::create(XYZ{-4, 3, frac*15}, XYZ{0, 5, 7.5}, XYZ{4, 3, (1-frac)*15})
+    );
+
+    // counter-clockwise inputs
+    checkIntersection(
+        XYZ{5, 0, 15}, XYZ{0, 5, 7.5}, XYZ{-5, 0, 0},
+        XY{4, 3}, XY{0, 5}, XY{-4, 3},
+           CircularArcIntersector::COCIRCULAR_INTERSECTION,
+                CircularArc::create(XYZ{4, 3, (1-frac)*15}, XYZ{0, 5, 7.5}, XYZ{-4, 3, frac*15})
+    );
+}
+
+template<>
+template<>
+void object::test<79>() {
+    set_test_name("XYZ arc intersected with a subset of itself that has different Z values");
+    // Related:: RobustLineIntersectorTest::testCollinearContainedDifferentZ
+
+    // clockwise inputs
+    checkIntersection(
+        XYZ{-5, 0, 0}, XYZ{0, 5, 7.5}, XYZ{5, 0, 15},
+        XYZ{-4, 3, 100}, XYZ{0, 5, 150}, XYZ{4, 3, 200},
+           CircularArcIntersector::COCIRCULAR_INTERSECTION,
+                CircularArc::create(XYZ{-4, 3, 100}, XYZ{0, 5, 150}, XYZ{4, 3, 200})
+    );
+
+    // counter-clockwise inputs
+    checkIntersection(
+        XYZ{5, 0, 15}, XYZ{0, 5, 7.5}, XYZ{-5, 0, 0},
+        XYZ{4, 3, 200}, XYZ{0, 5, 150}, XYZ{-4, 3, 100},
+           CircularArcIntersector::COCIRCULAR_INTERSECTION,
+                CircularArc::create(XYZ{4, 3, 200}, XYZ{0, 5, 150}, XYZ{-4, 3, 100})
+    );
+}
+
+template<>
+template<>
+void object::test<80>() {
+    set_test_name("XYZ arc intersected with a superset of itself that has different Z values");
+
+    // clockwise inputs
+    checkIntersection(
+        XYZ{-4, 3, 100}, XYZ{0, 5, 150}, XYZ{4, 3, 200},
+        XYZ{-5, 0, 0}, XYZ{0, 5, 7.5}, XYZ{5, 0, 15},
+           CircularArcIntersector::COCIRCULAR_INTERSECTION,
+                CircularArc::create(XYZ{-4, 3, 100}, XYZ{0, 5, 150}, XYZ{4, 3, 200})
+    );
+
+    // counter-clockwise inputs
+    checkIntersection(
+        XYZ{4, 3, 200}, XYZ{0, 5, 150}, XYZ{-4, 3, 100}, 
+        XYZ{5, 0, 15},  XYZ{0, 5, 7.5}, XYZ{-5, 0, 0},
+           CircularArcIntersector::COCIRCULAR_INTERSECTION,
+                CircularArc::create(XYZ{4, 3, 200}, XYZ{0, 5, 150}, XYZ{-4, 3, 100})
+    );
+
+}
+
+template<>
+template<>
+void object::test<81>
+()
+{
+    // Interior intersection of two XYZM lines.
+    // Result Z and M are the average of the interpolated coordinate values.
+    set_test_name("XYZM segment and XYZM segment with interior intersection");
+    // Related:: RobustLineIntersectorTest::testInteriorXYZM-XYZM
+
+    checkIntersectionSegSeg(
+            XYZM{1, 1, 1, -1}, XYZM{3, 3, 3, -3},
+            XYZM{1, 3, 10, -10}, XYZM{3, 1, 30, -30},
+            CircularArcIntersector::ONE_POINT_INTERSECTION,
+            XYZM{2, 2, 11, -11}
+    );
+}
+
+// TODO: check Z values of arc result centerpoints
+// TODO: add tests for seg/seg
+
+}
diff --git a/tests/unit/algorithm/CircularArcsTest.cpp b/tests/unit/algorithm/CircularArcsTest.cpp
index 96082078b..ccb3e4979 100644
--- a/tests/unit/algorithm/CircularArcsTest.cpp
+++ b/tests/unit/algorithm/CircularArcsTest.cpp
@@ -15,6 +15,8 @@ namespace tut {
 struct test_circulararcs_data {
     const double eps = 1e-8;
 
+    using XY = CoordinateXY;
+
     void checkEnvelope(const CoordinateXY& p0, const CoordinateXY& p1, const CoordinateXY& p2,
                        double xmin, double ymin, double xmax, double ymax)
     {
@@ -39,21 +41,17 @@ struct test_circulararcs_data {
         }
     }
 
-    static std::string toWKT(const CoordinateXY& p0, const CoordinateXY& p1, const CoordinateXY& p2)
-    {
-        std::stringstream ss;
-        ss << "CIRCULARSTRING (" << p0 << ", " << p1 << ", " << p2 << ")";
-        return ss.str();
-    }
-
-    void checkArc(std::string message,
-                  const CoordinateXY& center, double radius, bool ccw, double from, double to,
+    void checkArc(const std::string& message,
+                  const CoordinateXY& center, double radius, int orientation, double from, double to,
                   const CoordinateXY& p0, const CoordinateXY& p1, const CoordinateXY& p2) const
     {
-        CircularArc arc(from, to, center, radius, ccw);
+        CoordinateXY fromPt = CircularArcs::createPoint(center, radius, from);
+        CoordinateXY toPt = CircularArcs::createPoint(center, radius, to);
 
-        if (arc.p0.distance(p0) > eps || arc.p1.distance(p1) > eps || arc.p2.distance(p2) > eps) {
-            ensure_equals(message, toWKT(arc.p0, arc.p1, arc.p2), toWKT(p0, p1, p2));
+        auto arc = CircularArc::create(fromPt, toPt, center, radius, orientation);
+
+        if (arc.p0().distance(p0) > eps || arc.p1().distance(p1) > eps || arc.p2().distance(p2) > eps) {
+            ensure_equals(message, arc.toString(), CircularArc::create(p0, p1, p2).toString());
         }
     }
 
@@ -256,8 +254,8 @@ void object::test<15>()
 {
     set_test_name("createArc");
 
-    constexpr bool CCW = true;
-    constexpr bool CW = false;
+    auto CCW = geos::algorithm::Orientation::COUNTERCLOCKWISE;
+    auto CW = geos::algorithm::Orientation::CLOCKWISE;
 
     checkArc("CCW: upper half-circle", {0, 0}, 1, CCW, 0, MATH_PI, {1, 0}, {0, 1}, {-1, 0});
     checkArc("CCW: lower half-circle", {0, 0}, 1, CCW, MATH_PI, 0, {-1, 0}, {0, -1}, {1, 0});
@@ -270,37 +268,39 @@ void object::test<15>()
     checkArc("CW: right half-circle", {0, 0}, 1, CW, MATH_PI/2, -MATH_PI/2, {0, 1}, {1, 0}, {0, -1});
 }
 
+#if 0
 template<>
 template<>
 void object::test<16>()
 {
     set_test_name("splitAtPoint");
 
-    CircularArc cwArc({-1, 0}, {0, 1}, {1, 0});
+    CircularArc cwArc(XY{-1, 0}, XY{0, 1}, XY{1, 0});
     auto [arc1, arc2] = cwArc.splitAtPoint({std::sqrt(2)/2, std::sqrt(2)/2});
 
-    ensure_equals(arc1.p0, CoordinateXY{-1, 0});
-    ensure_equals(arc1.p2, CoordinateXY{std::sqrt(2)/2, std::sqrt(2)/2});
+    ensure_equals(arc1.p0(), CoordinateXY{-1, 0});
+    ensure_equals(arc1.p2(), CoordinateXY{std::sqrt(2)/2, std::sqrt(2)/2});
     ensure_equals(arc1.getCenter(), cwArc.getCenter());
     ensure_equals(arc1.getRadius(), cwArc.getRadius());
 
-    ensure_equals(arc2.p0, CoordinateXY{std::sqrt(2)/2, std::sqrt(2)/2});
-    ensure_equals(arc2.p2, CoordinateXY{1, 0});
+    ensure_equals(arc2.p0(), CoordinateXY{std::sqrt(2)/2, std::sqrt(2)/2});
+    ensure_equals(arc2.p2(), CoordinateXY{1, 0});
     ensure_equals(arc2.getCenter(), cwArc.getCenter());
     ensure_equals(arc2.getRadius(), cwArc.getRadius());
 
     ensure_equals(cwArc.getLength(), arc1.getLength() + arc2.getLength());
 }
+#endif
 
 template<>
 template<>
 void object::test<17>() {
     set_test_name("getSagitta");
 
-    CircularArc halfCircle({-1, 0}, {0, 1}, {1, 0});
+    CircularArc halfCircle = CircularArc::create(XY{-1, 0}, XY{0, 1}, XY{1, 0});
     ensure_equals(halfCircle.getSagitta(), 1);
 
-    CircularArc quarterCircle({0, 1}, {std::sqrt(2)/2, std::sqrt(2)/2}, {1, 0});
+    CircularArc quarterCircle = CircularArc::create(XY{0, 1}, XY{std::sqrt(2)/2, std::sqrt(2)/2}, {1, 0});
     ensure_equals(quarterCircle.getSagitta(),
         CoordinateXY{std::sqrt(2)/2, std::sqrt(2)/2}.distance(CoordinateXY{0.5, 0.5}));
 }
diff --git a/tests/unit/algorithm/RobustLineIntersectorZTest.cpp b/tests/unit/algorithm/RobustLineIntersectorZTest.cpp
index 7d7936bc8..2c64c100d 100644
--- a/tests/unit/algorithm/RobustLineIntersectorZTest.cpp
+++ b/tests/unit/algorithm/RobustLineIntersectorZTest.cpp
@@ -168,7 +168,7 @@ void object::test<4>
 ()
 {
     // XY intersects XYZ at interior point.
-    // Z value at the intersection point is the interpolated value from the XZZ line.
+    // Z value at the intersection point is the interpolated value from the XYZ line.
     set_test_name("testInterior2D3D");
 
     checkIntersection(
diff --git a/tests/unit/capi/GEOSNodeTest.cpp b/tests/unit/capi/GEOSNodeTest.cpp
index 41324bda5..3fe730d9a 100644
--- a/tests/unit/capi/GEOSNodeTest.cpp
+++ b/tests/unit/capi/GEOSNodeTest.cpp
@@ -1,11 +1,14 @@
 //
 // Test Suite for C-API GEOSNode
 
+#include <iostream>
 #include <tut/tut.hpp>
 // geos
 #include <geos_c.h>
+#include <geos/geom/Geometry.h>
 
 #include "capi_test_utils.h"
+#include "utility.h"
 
 namespace tut {
 //
@@ -200,12 +203,60 @@ template<>
 template<>
 void object::test<9>()
 {
+    set_test_name("two arcs with two intersection points");
+
     input_ = fromWKT("MULTICURVE (CIRCULARSTRING (0 0, 1 1, 2 0), CIRCULARSTRING (0 1, 1 0, 2 1))");
     ensure(input_);
 
     result_ = GEOSNode(input_);
+    ensure(result_ != nullptr);
 
-    ensure("curved geometries not supported", result_ == nullptr);
+    expected_ = fromWKT("MULTICURVE ("
+                        "CIRCULARSTRING (0 0, 0.0340741737 0.2588190451, 0.1339745962 0.5, 1 1, 1.8660254038 0.5, 1.9659258263 0.2588190451, 2 0),"
+                        "CIRCULARSTRING (0 1, 0.0340741737 0.7411809549, 0.1339745962 0.5, 1 0, 1.8660254038 0.5, 1.9659258263 0.7411809549, 2 1))");
+
+    ensure_geometry_equals_exact(result_, expected_, 1e-8);
+}
+
+template<>
+template<>
+void object::test<10>()
+{
+    set_test_name("CIRCULARSTRING ZM intersecting CIRCULARSTRING M");
+
+    input_ = fromWKT("MULTICURVE (CIRCULARSTRING ZM (-1 0 3 4, 0 1 2 5, 1 0 4 7), CIRCULARSTRING M (-1 2 9, 0 1 13, -1 0 17))");
+    ensure(input_);
+
+    result_ = GEOSNode(input_);
+    ensure(result_ != nullptr);
+
+    expected_ = fromWKT("MULTICURVE ZM ("
+                        "CIRCULARSTRING ZM (-1 0 3 4, -1 1.2246467991e-16 5 4.75, -1 2.7755575616e-16 7 5.5, -1 1.2246467991e-16 7 5.5, -1 5.5511151231e-17 7 5.5, -0.7071067812 0.7071067812 5.25 7.375, -2.7755575616e-16 1 3.5 9.25, -3.8285686989e-16 1 3.5 9.25, -5.5511151231e-17 1 3.5 9.25, 0.7071067812 0.7071067812 3.75 8.125, 1 0 4 7),"
+                        "CIRCULARSTRING ZM (-1 2 NaN 9, -0.2928932188 1.7071067812 NaN 9.125, -2.7755575616e-16 1 3.5 9.25, 0 1 3.5 9.25, -5.5511151231e-17 1 3.5 9.25, -0.2928932188 0.2928932188 5.25 7.375, -1 2.7755575616e-16 7 5.5, -1 0 7 5.5, -1 5.5511151231e-17 7 5.5, -1 0 NaN 11.25, -1 0 NaN 17))");
+
+    ensure_equals_exact_geometry_xyzm(reinterpret_cast<Geometry*>(result_),
+                                      reinterpret_cast<Geometry*>(expected_), 1e-8);
+}
+
+template<>
+template<>
+void object::test<11>()
+{
+    set_test_name("CIRCULARSTRING ZM intersecting LINESTRING M");
+
+    input_ = fromWKT("MULTICURVE (CIRCULARSTRING ZM (-5 0 3 4, -4 3 2 5, 4 3 4 7), LINESTRING M (0 0 7, 0 10 13))");
+    ensure(input_);
+
+    result_ = GEOSNode(input_);
+    ensure(result_ != nullptr);
+
+    expected_ = fromWKT("MULTICURVE ZM ("
+                        "CIRCULARSTRING ZM (-5 0 3 4, -3.5355 3.5355 3 6, 0 5 3 8, 3.5355 3.5355 3.5 7.5, 4 3 4 7),"
+                        "LINESTRING ZM (0 0 NaN 7, 0 5 3 8),"
+                        "LINESTRING ZM (0 5 3 8, 0 10 NaN 13))");
+
+    ensure_equals_exact_geometry_xyzm(reinterpret_cast<Geometry*>(result_),
+                                      reinterpret_cast<Geometry*>(expected_), 1e-4);
 }
 
 } // namespace tut
diff --git a/tests/unit/geom/CircularArcTest.cpp b/tests/unit/geom/CircularArcTest.cpp
index 024820bb6..d09c9ae5b 100644
--- a/tests/unit/geom/CircularArcTest.cpp
+++ b/tests/unit/geom/CircularArcTest.cpp
@@ -16,19 +16,19 @@ struct test_circulararc_data {
 
     void checkAngle(const CoordinateXY& p0, const CoordinateXY& p1, const CoordinateXY& p2, double expected)
     {
-        CircularArc arc(p0, p1, p2);
+        auto arc = CircularArc::create(p0, p1, p2);
         ensure_equals(p0.toString() + " / " + p1.toString() + " / " + p2.toString(), arc.getAngle(), expected, eps);
 
-        CircularArc rev(p2, p1, p0);
+        auto rev = CircularArc::create(p2, p1, p0);
         ensure_equals(p2.toString() + " / " + p1.toString() + " / " + p0.toString(), rev.getAngle(), expected, eps);
     }
 
     void checkLength(const CoordinateXY& p0, const CoordinateXY& p1, const CoordinateXY& p2, double expected)
     {
-        CircularArc arc(p0, p1, p2);
+        auto arc = CircularArc::create(p0, p1, p2);
         ensure_equals(p0.toString() + " / " + p1.toString() + " / " + p2.toString(), arc.getLength(), expected, eps);
 
-        CircularArc rev(p2, p1, p0);
+        auto rev = CircularArc::create(p2, p1, p0);
         ensure_equals(p2.toString() + " / " + p1.toString() + " / " + p0.toString(), rev.getLength(), expected, eps);
     }
 };
@@ -83,14 +83,14 @@ void object::test<3>()
 {
     set_test_name("CircularArc::getArea()");
 
-    ensure_equals("half circle, R=2", CircularArc({-2, 0}, {0, 2}, {2, 0}).getArea(), MATH_PI*2);
+    ensure_equals("half circle, R=2", CircularArc::create(CoordinateXY{-2, 0}, {0, 2}, {2, 0}).getArea(), MATH_PI*2);
 
-    ensure_equals("full circle, R=3", CircularArc({-3, 0}, {3, 0}, {-3, 0}).getArea(), MATH_PI*3*3);
+    ensure_equals("full circle, R=3", CircularArc::create(CoordinateXY{-3, 0}, {3, 0}, {-3, 0}).getArea(), MATH_PI*3*3);
 
-    ensure_equals("3/4, mouth up, R=2", CircularArc({-std::sqrt(2), std::sqrt(2)}, {0, -2}, {std::sqrt(2), std::sqrt(2)}).getArea(),
+    ensure_equals("3/4, mouth up, R=2", CircularArc::create(CoordinateXY{-std::sqrt(2), std::sqrt(2)}, {0, -2}, {std::sqrt(2), std::sqrt(2)}).getArea(),
                   MATH_PI*4 - 2*(MATH_PI/2-1), 1e-8);
 
-    ensure_equals("1/4, pointing up, R=2", CircularArc({-std::sqrt(2), std::sqrt(2)}, {0, 2}, {std::sqrt(2), std::sqrt(2)}).getArea(),
+    ensure_equals("1/4, pointing up, R=2", CircularArc::create(CoordinateXY{-std::sqrt(2), std::sqrt(2)}, {0, 2}, {std::sqrt(2), std::sqrt(2)}).getArea(),
                   2*(MATH_PI/2-1), 1e-8);
 }
 
@@ -100,8 +100,8 @@ void object::test<4>()
 {
     set_test_name("CircularArc::isLinear()");
 
-    ensure_equals("not linear", CircularArc({-1, 0}, {0, 1}, {1, 0}).isLinear(), false);
-    ensure_equals("linear", CircularArc({0, 0}, {1, 1}, {2, 2}).isLinear(), true);
+    ensure_equals("not linear", CircularArc::create(CoordinateXY{-1, 0}, {0, 1}, {1, 0}).isLinear(), false);
+    ensure_equals("linear", CircularArc::create(CoordinateXY{0, 0}, {1, 1}, {2, 2}).isLinear(), true);
 }
 
 template<>
@@ -110,15 +110,17 @@ void object::test<5>()
 {
     set_test_name("CircularArc::containsPointOnCircle");
 
+    // FIXME: Add asserts
+
     // complete circle
-    CircularArc({5, 0}, {-5, 0}, {5, 0}).containsPointOnCircle({5, 0});
-    CircularArc({5, 0}, {-5, 0}, {5, 0}).containsPointOnCircle({4, 3});
+    CircularArc::create(CoordinateXY{5, 0}, {-5, 0}, {5, 0}).containsPointOnCircle({5, 0});
+    CircularArc::create(CoordinateXY{5, 0}, {-5, 0}, {5, 0}).containsPointOnCircle({4, 3});
 
     // lower semi-circle
-    CircularArc({-5, 0}, {0, -5}, {5, 0}).containsPointOnCircle({5, 0});
+    CircularArc::create(CoordinateXY{-5, 0}, {0, -5}, {5, 0}).containsPointOnCircle({5, 0});
 
     // upper semi-circle
-    CircularArc({-5, 0}, {0, 5}, {5, 0}).containsPointOnCircle({5, 0});
+    CircularArc::create(CoordinateXY{-5, 0}, {0, 5}, {5, 0}).containsPointOnCircle({5, 0});
 }
 
 }
diff --git a/tests/unit/geom/CoordinateSequenceTest.cpp b/tests/unit/geom/CoordinateSequenceTest.cpp
index a0c86888e..89c35ba8d 100644
--- a/tests/unit/geom/CoordinateSequenceTest.cpp
+++ b/tests/unit/geom/CoordinateSequenceTest.cpp
@@ -1617,4 +1617,29 @@ void object::test<61>()
     ensure(std::isnan(seq.getM(1)));
 }
 
+template<>
+template<>
+void object::test<62>
+()
+{
+    set_test_name("CoordinateSequence::swap");
+
+    CoordinateSequence seq = CoordinateSequence::XYZM(0);
+    CoordinateXYZM p0{1,2,3,4};
+    CoordinateXYZM p1{5,6,7,8};
+    CoordinateXYZM p2{9,10,11,12};
+    seq.add(p0);
+    seq.add(p1);
+    seq.add(p2);
+
+    seq.swap(1, 1);
+    ensure_equals_xyzm(seq.getAt<CoordinateXYZM>(0), p0);
+    ensure_equals_xyzm(seq.getAt<CoordinateXYZM>(1), p1);
+    ensure_equals_xyzm(seq.getAt<CoordinateXYZM>(2), p2);
+
+    seq.swap(2, 1);
+    ensure_equals_xyzm(seq.getAt<CoordinateXYZM>(0), p0);
+    ensure_equals_xyzm(seq.getAt<CoordinateXYZM>(1), p2);
+    ensure_equals_xyzm(seq.getAt<CoordinateXYZM>(2), p1);
+}
 } // namespace tut
diff --git a/tests/unit/geom/CoordinateTest.cpp b/tests/unit/geom/CoordinateTest.cpp
index 1531fe845..b638552f3 100644
--- a/tests/unit/geom/CoordinateTest.cpp
+++ b/tests/unit/geom/CoordinateTest.cpp
@@ -439,6 +439,32 @@ void object::test<17>()
     ensure_same(xyzm.get<Ordinate::M>(), 4);
 }
 
+template<>
+template<>
+void object::test<18>()
+{
+    set_test_name("Coordinate::has()");
+
+    ensure_equals(CoordinateXY::has<Ordinate::X>(), true);
+    ensure_equals(CoordinateXY::has<Ordinate::Y>(), true);
+    ensure_equals(CoordinateXY::has<Ordinate::Z>(), false);
+    ensure_equals(CoordinateXY::has<Ordinate::M>(), false);
+
+    ensure_equals(Coordinate::has<Ordinate::X>(), true);
+    ensure_equals(Coordinate::has<Ordinate::Y>(), true);
+    ensure_equals(Coordinate::has<Ordinate::Z>(), true);
+    ensure_equals(Coordinate::has<Ordinate::M>(), false);
+
+    ensure_equals(CoordinateXYM::has<Ordinate::X>(), true);
+    ensure_equals(CoordinateXYM::has<Ordinate::Y>(), true);
+    ensure_equals(CoordinateXYM::has<Ordinate::Z>(), false);
+    ensure_equals(CoordinateXYM::has<Ordinate::M>(), true);
+
+    ensure_equals(CoordinateXYZM::has<Ordinate::X>(), true);
+    ensure_equals(CoordinateXYZM::has<Ordinate::Y>(), true);
+    ensure_equals(CoordinateXYZM::has<Ordinate::Z>(), true);
+    ensure_equals(CoordinateXYZM::has<Ordinate::M>(), true);
+}
 
 } // namespace tut
 
diff --git a/tests/unit/noding/NodableArcStringTest.cpp b/tests/unit/noding/NodableArcStringTest.cpp
index bde0f4970..d468b1d46 100644
--- a/tests/unit/noding/NodableArcStringTest.cpp
+++ b/tests/unit/noding/NodableArcStringTest.cpp
@@ -5,18 +5,25 @@
 
 using geos::algorithm::Orientation;
 using geos::geom::CircularArc;
+using geos::geom::Ordinate;
 using geos::geom::CoordinateXY;
+using geos::geom::CoordinateXYZM;
 using geos::noding::NodableArcString;
 
 namespace tut {
 
 struct test_nodablearcstring_data {
 
+    using XY = CoordinateXY;
+    using XYZ = Coordinate;
+    using XYM = geos::geom::CoordinateXYM;
+    using XYZM = CoordinateXYZM;
+
     static void test_add_points(const CircularArc& arc, const std::vector<CoordinateXY>& coords,
                                 const std::vector<CircularArc>& expected, bool reversed=false) {
         std::vector<CircularArc> arcs;
         arcs.push_back(arc);
-        NodableArcString nas(arcs);
+        NodableArcString nas(arcs, nullptr, false, false, nullptr);
 
         for (const auto& coord : coords) {
             nas.addInt(coord, 0);
@@ -27,16 +34,15 @@ struct test_nodablearcstring_data {
         ensure_equals(noded->getSize(), expected.size());
 
         for (std::size_t i = 0; i < expected.size(); i++) {
-            ensure_arc_equals(noded->getArc(i), expected[i]);
+            ensure_arc_equals(noded->getArc(i), expected[i], 1e-8);
         }
 
         if (!reversed) {
-            auto revArc = arc;
-            revArc.reverse();
+            const auto revArc = arc.reverse();
 
-            auto revExpected = expected;
-            for (auto& x : revExpected) {
-                x.reverse();
+            std::vector<CircularArc> revExpected;
+            for (const auto& x : expected) {
+                revExpected.push_back(x.reverse());
             }
             std::reverse(revExpected.begin(), revExpected.end());
 
@@ -44,12 +50,8 @@ struct test_nodablearcstring_data {
         }
     }
 
-    static void ensure_arc_equals(const CircularArc& actual, const CircularArc& expected) {
-        ensure_equals_xy(actual.p0, expected.p0);
-        ensure_equals_xy(actual.p2, expected.p2);
-        ensure_equals_xy(actual.getCenter(), expected.getCenter());
-        ensure_equals(actual.getRadius(), expected.getRadius());
-        ensure_equals(actual.getOrientation(), expected.getOrientation());
+    static void ensure_arc_equals(const CircularArc& actual, const CircularArc& expected, double tol) {
+        ensure(actual.toString() + " does not equal expected " + expected.toString() ,actual.equals(expected, tol));
     }
 };
 
@@ -64,7 +66,7 @@ void object::test<1>()
 {
     set_test_name("CW half-circle, upper half-plane");
 
-    CircularArc in(CoordinateXY{-5, 0}, CoordinateXY{0, 5}, CoordinateXY{5, 0});
+    CircularArc in = CircularArc::create(CoordinateXY{-5, 0}, CoordinateXY{0, 5}, CoordinateXY{5, 0});
 
     std::vector<CoordinateXY> coords;
     coords.emplace_back(4, 3);
@@ -73,11 +75,11 @@ void object::test<1>()
     coords.emplace_back(-4, 3);
 
     std::vector<CircularArc> expected;
-    expected.push_back(CircularArc({-5, 0}, {-4, 3}, {0, 0}, 5, Orientation::CLOCKWISE));
-    expected.push_back(CircularArc({-4, 3}, {-3, 4}, {0, 0}, 5, Orientation::CLOCKWISE));
-    expected.push_back(CircularArc({-3, 4}, {3, 4}, {0, 0}, 5, Orientation::CLOCKWISE));
-    expected.push_back(CircularArc({3, 4}, {4, 3}, {0, 0}, 5, Orientation::CLOCKWISE));
-    expected.push_back(CircularArc({4, 3}, {5, 0}, {0, 0}, 5, Orientation::CLOCKWISE));
+    expected.push_back(CircularArc::create(XY{-5, 0}, {-4, 3}, {0, 0}, 5, Orientation::CLOCKWISE));
+    expected.push_back(CircularArc::create(XY{-4, 3}, {-3, 4}, {0, 0}, 5, Orientation::CLOCKWISE));
+    expected.push_back(CircularArc::create(XY{-3, 4}, {3, 4}, {0, 0}, 5, Orientation::CLOCKWISE));
+    expected.push_back(CircularArc::create(XY{3, 4}, {4, 3}, {0, 0}, 5, Orientation::CLOCKWISE));
+    expected.push_back(CircularArc::create(XY{4, 3}, {5, 0}, {0, 0}, 5, Orientation::CLOCKWISE));
 
     test_add_points(in, coords, expected);
 }
@@ -88,7 +90,7 @@ void object::test<2>()
 {
     set_test_name("CW half-circle, right half-plane");
 
-    CircularArc in(CoordinateXY{0, 5}, CoordinateXY{5, 0}, CoordinateXY{0, -5});
+    CircularArc in = CircularArc::create(CoordinateXY{0, 5}, CoordinateXY{5, 0}, CoordinateXY{0, -5});
 
     std::vector<CoordinateXY> coords;
     coords.emplace_back(4, -3);
@@ -98,14 +100,65 @@ void object::test<2>()
     coords.emplace_back(5, 0);
 
     std::vector<CircularArc> expected;
-    expected.push_back(CircularArc({0, 5}, {3, 4}, {0, 0}, 5, Orientation::CLOCKWISE));
-    expected.push_back(CircularArc({3, 4}, {4, 3}, {0, 0}, 5, Orientation::CLOCKWISE));
-    expected.push_back(CircularArc({4, 3}, {5, 0}, {0, 0}, 5, Orientation::CLOCKWISE));
-    expected.push_back(CircularArc({5, 0}, {4, -3}, {0, 0}, 5, Orientation::CLOCKWISE));
-    expected.push_back(CircularArc({4, -3}, {3, -4}, {0, 0}, 5, Orientation::CLOCKWISE));
-    expected.push_back(CircularArc({3, -4}, {0, -5}, {0, 0}, 5, Orientation::CLOCKWISE));
+    expected.push_back(CircularArc::create(XY{0, 5}, {3, 4}, {0, 0}, 5, Orientation::CLOCKWISE));
+    expected.push_back(CircularArc::create(XY{3, 4}, {4, 3}, {0, 0}, 5, Orientation::CLOCKWISE));
+    expected.push_back(CircularArc::create(XY{4, 3}, {5, 0}, {0, 0}, 5, Orientation::CLOCKWISE));
+    expected.push_back(CircularArc::create(XY{5, 0}, {4, -3}, {0, 0}, 5, Orientation::CLOCKWISE));
+    expected.push_back(CircularArc::create(XY{4, -3}, {3, -4}, {0, 0}, 5, Orientation::CLOCKWISE));
+    expected.push_back(CircularArc::create(XY{3, -4}, {0, -5}, {0, 0}, 5, Orientation::CLOCKWISE));
 
     test_add_points(in, coords, expected);
 }
 
+template<>
+template<>
+void object::test<3>()
+{
+    set_test_name("no points added");
+    CircularArc in = CircularArc::create(CoordinateXY{-1, 0}, CoordinateXY{0, 1}, CoordinateXY{1, 0});
+
+    std::vector<CoordinateXY> coords;
+    std::vector<CircularArc> expected;
+    expected.push_back(in);
+    test_add_points(in, coords, expected);
+}
+
+template<>
+template<>
+void object::test<4>()
+{
+    set_test_name("Center point Z/M in constructed arcs interpolated from endpoints");
+
+    CoordinateSequence seq = CoordinateSequence::XYZM(3);
+    CoordinateXYZM p0{0, 5, 6, 2};
+    CoordinateXYZM p1{5, 0, 7, 3};
+    CoordinateXYZM p2{4, -3, 9, 1};
+
+    seq.setAt(p0, 0);
+    seq.setAt(p1, 1);
+    seq.setAt(p2, 2);
+
+    CircularArc arc (seq, 0);
+
+    CoordinateXYZM intPt{4, 3, 13, 5};
+
+    std::vector<CircularArc> in { arc };
+    NodableArcString nas(std::move(in), nullptr, true, true, nullptr);
+
+    nas.addIntersection( intPt, 0);
+
+    auto noded = nas.getNoded();
+
+    ensure_equals(noded->getSize(), 2u);
+    const CircularArc& arc0 = noded->getArc(0);
+    ensure_arc_equals(arc0, CircularArc::create(p0, intPt, arc.getCenter(), arc.getRadius(), arc.getOrientation()), 1e-8);
+    ensure_equals(arc0.p1<CoordinateXYZM>().z, (p0.z + intPt.z) / 2);
+    ensure_equals(arc0.p1<CoordinateXYZM>().m, (p0.m + intPt.m) / 2);
+
+    const CircularArc& arc1 = noded->getArc(1);
+    ensure_arc_equals(arc1, CircularArc::create(intPt, p2, arc.getCenter(), arc.getRadius(), arc.getOrientation()), 1e-8);
+    ensure_equals(arc1.p1<CoordinateXYZM>().z, (intPt.z + p2.z) / 2);
+    ensure_equals(arc1.p1<CoordinateXYZM>().m, (intPt.m + p2.m) / 2);
+}
+
 }
\ No newline at end of file
diff --git a/tests/unit/noding/SimpleNoderTest.cpp b/tests/unit/noding/SimpleNoderTest.cpp
index 7450724c6..401763bef 100644
--- a/tests/unit/noding/SimpleNoderTest.cpp
+++ b/tests/unit/noding/SimpleNoderTest.cpp
@@ -12,6 +12,7 @@
 using geos::geom::CoordinateXY;
 using geos::geom::CoordinateSequence;
 using geos::geom::CircularArc;
+using geos::geom::Ordinate;
 using geos::algorithm::Orientation;
 using geos::noding::ArcString;
 using geos::noding::SegmentString;
@@ -24,6 +25,20 @@ namespace tut {
 
 struct test_simplenoder_data {
 
+    // FIXME: This is duplicated from CircularArcIntersectorTest
+    template<typename T>
+    CircularArc makeArc(T p0, T p2, const CoordinateXY& center, double radius, int orientation)
+    {
+        auto seq = std::make_unique<CoordinateSequence>(3, T::template has<Ordinate::Z>(), T::template has<Ordinate::M>());
+        seq->setAt(p0, 0);
+        seq->setAt(geos::algorithm::CircularArcs::getMidpoint(p0, p2, center, radius, orientation == Orientation::COUNTERCLOCKWISE), 1);
+        seq->setAt(p2, 2);
+
+        CircularArc ret(std::move(seq), 0);
+
+        return ret;
+    }
+
     template<typename T1, typename T2>
     static void
     check_length_equal(const T1& actual, const T2& expected) {
@@ -76,11 +91,11 @@ struct test_simplenoder_data {
                 for (const auto& arc : *arcString) {
                     if (first) {
                         first = false;
-                        std::cout << arc.p0 << ", ";
+                        std::cout << arc.p0() << ", ";
                     } else {
                         std::cout << ", ";
                     }
-                    std::cout << arc.p1 << ", " << arc.p2;
+                    std::cout << arc.p1() << ", " << arc.p2();
                 }
                 std::cout << ")";
             }
@@ -128,11 +143,14 @@ void object::test<2>()
 {
     set_test_name("arc-arc intersection");
 
-    CircularArc arc0({-1, 0}, {1, 0}, {0, 0}, 1, Orientation::CLOCKWISE);
-    CircularArc arc1({-1, 1}, {1, 1}, {0, 1}, 1, Orientation::COUNTERCLOCKWISE);
+    std::vector<CircularArc> arcs0;
+    arcs0.push_back(makeArc(CoordinateXY{-1, 0}, {1, 0}, {0, 0}, 1, Orientation::CLOCKWISE));
 
-    NodableArcString as0({arc0});
-    NodableArcString as1({arc1});
+    std::vector<CircularArc> arcs1;
+    arcs1.push_back(makeArc(CoordinateXY{-1, 1}, {1, 1}, {0, 1}, 1, Orientation::COUNTERCLOCKWISE));
+
+    NodableArcString as0(std::move(arcs0), nullptr, false, false, nullptr);
+    NodableArcString as1(std::move(arcs1), nullptr, false, false, nullptr);
 
     std::vector<PathString*> ss{&as0, &as1};
 
@@ -150,8 +168,9 @@ void object::test<3>()
 {
     set_test_name("arc-segment intersection");
 
-    CircularArc arc0({-1, 0}, {1, 0}, {0, 0}, 1, Orientation::CLOCKWISE);
-    NodableArcString as0({arc0});
+    std::vector<CircularArc> arcs0;
+    arcs0.push_back(makeArc(CoordinateXY{-1, 0}, {1, 0}, {0, 0}, 1, Orientation::CLOCKWISE));
+    NodableArcString as0(std::move(arcs0), nullptr, false, false, nullptr);
 
     auto seq1 = std::make_shared<CoordinateSequence>();
     seq1->add(CoordinateXY{-1, 0.5});
diff --git a/tests/unit/utility.h b/tests/unit/utility.h
index c86dbce56..6b98a59d0 100644
--- a/tests/unit/utility.h
+++ b/tests/unit/utility.h
@@ -131,45 +131,48 @@ ensure_equals_xy(geos::geom::CoordinateXY const& actual,
 
 inline void
 ensure_equals_xyz(geos::geom::Coordinate const& actual,
-                  geos::geom::Coordinate const& expected)
+                  geos::geom::Coordinate const& expected,
+                  double tol=0)
 {
-    ensure_equals("Coordinate X", actual.x, expected.x );
-    ensure_equals("Coordinate Y", actual.y, expected.y );
+    ensure_equals("Coordinate X", actual.x, expected.x, tol);
+    ensure_equals("Coordinate Y", actual.y, expected.y, tol);
     if ( std::isnan(expected.z) ) {
         ensure("Coordinate Z should be NaN", std::isnan(actual.z) );
     } else {
-        ensure_equals("Coordinate Z", actual.z, expected.z );
+        ensure_equals("Coordinate Z", actual.z, expected.z, tol);
     }
 }
 
 inline void
 ensure_equals_xym(geos::geom::CoordinateXYM const& actual,
-                  geos::geom::CoordinateXYM const& expected)
+                  geos::geom::CoordinateXYM const& expected,
+                  double tol=0)
 {
-    ensure_equals("Coordinate X", actual.x, expected.x );
-    ensure_equals("Coordinate Y", actual.y, expected.y );
+    ensure_equals("Coordinate X", actual.x, expected.x , tol);
+    ensure_equals("Coordinate Y", actual.y, expected.y, tol);
     if ( std::isnan(expected.m) ) {
         ensure("Coordinate M should be NaN", std::isnan(actual.m) );
     } else {
-        ensure_equals("Coordinate M", actual.m, expected.m );
+        ensure_equals("Coordinate M", actual.m, expected.m, tol);
     }
 }
 
 inline void
 ensure_equals_xyzm(geos::geom::CoordinateXYZM const& actual,
-                   geos::geom::CoordinateXYZM const& expected)
+                   geos::geom::CoordinateXYZM const& expected,
+                   double tol = 0)
 {
-    ensure_equals("Coordinate X", actual.x, expected.x );
-    ensure_equals("Coordinate Y", actual.y, expected.y );
+    ensure_equals("Coordinate X", actual.x, expected.x, tol);
+    ensure_equals("Coordinate Y", actual.y, expected.y, tol);
     if ( std::isnan(expected.z) ) {
         ensure("Coordinate Z should be NaN", std::isnan(actual.z) );
     } else {
-        ensure_equals("Coordinate Z", actual.z, expected.z );
+        ensure_equals("Coordinate Z", actual.z, expected.z, tol);
     }
     if ( std::isnan(expected.m) ) {
         ensure("Coordinate M should be NaN", std::isnan(actual.m) );
     } else {
-        ensure_equals("Coordinate M", actual.m, expected.m );
+        ensure_equals("Coordinate M", actual.m, expected.m, tol);
     }
 }
 

commit db93f778ec51fdc1a71886f2b319ec6da35a302d
Author: Daniel Baston <dbaston at gmail.com>
Date:   Sun Nov 2 18:15:09 2025 -0500

    SimpleNoder: Add Arc support

diff --git a/include/geos/algorithm/CircularArcIntersector.h b/include/geos/algorithm/CircularArcIntersector.h
index dad551906..c7a8c323b 100644
--- a/include/geos/algorithm/CircularArcIntersector.h
+++ b/include/geos/algorithm/CircularArcIntersector.h
@@ -82,10 +82,15 @@ public:
     static int
     circleIntersects(const CoordinateXY& center, double r, const CoordinateXY& p0, const CoordinateXY& p1, CoordinateXY& isect0, CoordinateXY& isect1);
 
-private:
 
     void intersects(const CoordinateXY& p0, const CoordinateXY& p1, const CoordinateXY& q0, const CoordinateXY& q1);
 
+private:
+    void reset() {
+        nPt = 0;
+        nArc = 0;
+    }
+
     std::array<CoordinateXY, 2> intPt;
     std::array<CircularArc, 2> intArc;
     intersection_type result = NO_INTERSECTION;
diff --git a/include/geos/geom/CircularArc.h b/include/geos/geom/CircularArc.h
index 621ab3ec1..be2b2fe43 100644
--- a/include/geos/geom/CircularArc.h
+++ b/include/geos/geom/CircularArc.h
@@ -250,6 +250,17 @@ public:
         return isUpward;
     }
 
+    void reverse() {
+        std::swap(p0, p2);
+        if (m_orientation_known) {
+            if (m_orientation == algorithm::Orientation::COUNTERCLOCKWISE) {
+                m_orientation = algorithm::Orientation::CLOCKWISE;
+            } else if (m_orientation == algorithm::Orientation::CLOCKWISE) {
+                m_orientation = algorithm::Orientation::COUNTERCLOCKWISE;
+            }
+        }
+    }
+
     // Split an arc at a specified point.
     // The point is assumed to be o the arc.
     std::pair<CircularArc, CircularArc> splitAtPoint(const CoordinateXY& q) const {
@@ -259,6 +270,10 @@ public:
         };
     }
 
+    std::string toString() const {
+        return "CIRCULARSTRING (" + p0.toString() + ", " + p1.toString() + ", " + p2.toString() + ")";
+    }
+
     class Iterator {
     public:
         using iterator_category = std::forward_iterator_tag;
diff --git a/include/geos/noding/ArcIntersectionAdder.h b/include/geos/noding/ArcIntersectionAdder.h
new file mode 100644
index 000000000..e3e55d904
--- /dev/null
+++ b/include/geos/noding/ArcIntersectionAdder.h
@@ -0,0 +1,36 @@
+/**********************************************************************
+*
+ * GEOS - Geometry Engine Open Source
+ * http://geos.osgeo.org
+ *
+ * Copyright (C) 2025 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/algorithm/CircularArcIntersector.h>
+#include <geos/noding/ArcIntersector.h>
+
+namespace geos::noding {
+
+class GEOS_DLL ArcIntersectionAdder : public ArcIntersector {
+
+public:
+    void processIntersections(ArcString& e0, std::size_t segIndex0, ArcString& e1, std::size_t segIndex1) override;
+
+    void processIntersections(ArcString& e0, std::size_t segIndex0, SegmentString& e1, std::size_t segIndex1) override;
+
+    void processIntersections(SegmentString& e0, std::size_t segIndex0, SegmentString& e1, std::size_t segIndex1) override;
+
+private:
+    algorithm::CircularArcIntersector m_intersector;
+};
+
+}
\ No newline at end of file
diff --git a/include/geos/noding/ArcIntersector.h b/include/geos/noding/ArcIntersector.h
new file mode 100644
index 000000000..7b5ee91aa
--- /dev/null
+++ b/include/geos/noding/ArcIntersector.h
@@ -0,0 +1,44 @@
+/**********************************************************************
+*
+ * GEOS - Geometry Engine Open Source
+ * http://geos.osgeo.org
+ *
+ * Copyright (C) 2025 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/noding/ArcString.h>
+#include <geos/noding/SegmentString.h>
+
+namespace geos::noding {
+
+class GEOS_DLL ArcIntersector {
+public:
+    virtual void processIntersections(ArcString& e0, std::size_t arcIndex0,
+                                      ArcString& e1, std::size_t arcIndex1) = 0;
+    virtual void processIntersections(SegmentString& e0, std::size_t segIndex0,
+                                      SegmentString& e1, std::size_t segIndex1) = 0;
+    virtual void processIntersections(ArcString& e0, std::size_t arcIndex0,
+                                      SegmentString& e1, std::size_t segIndex1) = 0;
+
+    virtual void processIntersections(SegmentString& e0, std::size_t segIndex0,
+                                      ArcString& e1, std::size_t arcIndex1) {
+        processIntersections(e1, arcIndex1, e0, segIndex0);
+    }
+
+    virtual bool isDone() const {
+        return false;
+    }
+
+    virtual ~ArcIntersector() = default;
+};
+
+}
\ No newline at end of file
diff --git a/include/geos/noding/ArcNoder.h b/include/geos/noding/ArcNoder.h
new file mode 100644
index 000000000..4337db295
--- /dev/null
+++ b/include/geos/noding/ArcNoder.h
@@ -0,0 +1,47 @@
+/**********************************************************************
+ *
+ * GEOS - Geometry Engine Open Source
+ * http://geos.osgeo.org
+ *
+ * Copyright (C) 2025 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/noding/ArcIntersector.h>
+#include <geos/noding/Noder.h>
+
+namespace geos::noding {
+class PathString;
+}
+
+namespace geos::noding {
+
+class GEOS_DLL ArcNoder : public Noder {
+
+public:
+    explicit ArcNoder(std::unique_ptr<ArcIntersector> intersector) :
+        m_intersector(std::move(intersector)) {}
+
+    ~ArcNoder() override;
+
+    void computeNodes(const std::vector<SegmentString*>& segStrings) override;
+
+    std::vector<std::unique_ptr<SegmentString>> getNodedSubstrings() override;
+
+    virtual void computePathNodes(const std::vector<PathString*>& inputPaths) = 0;
+
+    virtual std::vector<std::unique_ptr<PathString>> getNodedPaths() = 0;
+
+protected:
+    std::unique_ptr<ArcIntersector> m_intersector;
+};
+
+}
\ No newline at end of file
diff --git a/include/geos/noding/ArcString.h b/include/geos/noding/ArcString.h
new file mode 100644
index 000000000..10f254ed2
--- /dev/null
+++ b/include/geos/noding/ArcString.h
@@ -0,0 +1,63 @@
+/**********************************************************************
+*
+ * GEOS - Geometry Engine Open Source
+ * http://geos.osgeo.org
+ *
+ * Copyright (C) 2025 ISciences, LLC
+ *
+ * This is free software; you can redistribute and/or modify it under
+ * the terms of the GNU Lesser General Public Licence as published
+ * by the Free Software Foundation.
+ * See the COPYING file for more information.
+ *
+ **********************************************************************/
+
+#pragma once
+
+#include <geos/export.h>
+#include <geos/geom/CircularArc.h>
+#include <geos/noding/PathString.h>
+
+#include <vector>
+
+namespace geos::noding {
+
+/** \brief
+ *  An interface for classes which represent a sequence of contiguous
+ *  circular arcs, analogous to the SegmentString for contiguous line
+ *  segments.
+ */
+class GEOS_DLL ArcString : public PathString {
+public:
+    explicit ArcString(std::vector<geom::CircularArc> arcs) : m_arcs(std::move(arcs)) {
+    }
+
+    std::size_t getSize() const override {
+        return m_arcs.size();
+    }
+
+    double getLength() const override {
+        double tot = 0;
+        for (const auto &arc: m_arcs) {
+            tot += arc.getLength();
+        }
+        return tot;
+    }
+
+    const geom::CircularArc &getArc(std::size_t i) const {
+        return m_arcs[i];
+    }
+
+    auto begin() const {
+        return m_arcs.begin();
+    }
+
+    auto end() const {
+        return m_arcs.end();
+    }
+
+protected:
+    std::vector<geom::CircularArc> m_arcs;
+};
+
+}
\ No newline at end of file
diff --git a/include/geos/noding/NodableArcString.h b/include/geos/noding/NodableArcString.h
new file mode 100644
index 000000000..e1b612261
--- /dev/null
+++ b/include/geos/noding/NodableArcString.h
@@ -0,0 +1,104 @@
+/**********************************************************************
+*
+ * GEOS - Geometry Engine Open Source
+ * http://geos.osgeo.org
+ *
+ * Copyright (C) 2025 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/noding/ArcString.h>
+#include <geos/noding/NodablePath.h>
+
+namespace geos::noding {
+
+class GEOS_DLL NodableArcString : public ArcString, public NodablePath {
+
+public:
+    using ArcString::ArcString;
+
+    void addIntersection(geom::CoordinateXYZM intPt, size_t segmentIndex) override {
+        m_adds[segmentIndex].push_back(intPt);
+    }
+
+    static double pseudoAngleDiffCCW(double paStart, double pa) {
+        double diff = pa - paStart;
+
+        if (diff < 0) {
+            diff += 4;
+        }
+
+        return diff;
+    }
+
+    std::unique_ptr<ArcString> getNoded() {
+        if (m_adds.empty()) {
+            // use std::move ?
+            return std::make_unique<ArcString>(*this);
+        }
+
+        std::vector<geom::CircularArc> arcs;
+        for (size_t i = 0; i < m_arcs.size(); i++) {
+            auto it = m_adds.find(i);
+            if (it == m_adds.end()) {
+                arcs.push_back(m_arcs[i]);
+            } else {
+                std::vector<geom::CoordinateXYZM>& splitPoints = it->second;
+
+                // TODO check split point actually inside arc
+                // TODO ignore duplicate splitpoints
+
+                geom::CircularArc remainder = m_arcs[i];
+                const bool isCCW = remainder.getOrientation() == algorithm::Orientation::COUNTERCLOCKWISE;
+                const double paStart = geom::Quadrant::pseudoAngle(remainder.getCenter(), remainder.p0);
+                //double paStart = isCCW ? geom::Quadrant::pseudoAngle(remainder.getCenter(), remainder.p0) :
+                //                               geom::Quadrant::pseudoAngle(remainder.getCenter(), remainder.p2);
+
+                // Need a function for lengthFraction (pt);
+                std::sort(splitPoints.begin(), splitPoints.end(), [&remainder, paStart, isCCW](const auto& p0, const auto& p1) {
+                    double pa0 = geom::Quadrant::pseudoAngle(remainder.getCenter(), p0);
+                    double pa1 = geom::Quadrant::pseudoAngle(remainder.getCenter(), p1);
+
+                    // FIXME check this comparator...
+                    if (isCCW) {
+                        return pseudoAngleDiffCCW(paStart, pa0) < pseudoAngleDiffCCW(paStart, pa1);
+                    } else {
+                        return pseudoAngleDiffCCW(paStart, pa0) > pseudoAngleDiffCCW(paStart, pa1);
+                    }
+                });
+
+                std::cout << "arc " << remainder.toString() << " " << (isCCW ? "CCW" : "CW") << std::endl;
+                std::cout << "  paStart " << paStart << std::endl;
+                for (const auto& pt : splitPoints) {
+                    const double pa = geom::Quadrant::pseudoAngle(remainder.getCenter(), pt);
+                    std::cout << "  " << pt << " pa " << pa << " diff " << pseudoAngleDiffCCW(paStart, pa) << std::endl;
+                }
+
+                for (const auto& splitPoint : splitPoints) {
+                    if (!arcs.empty() && splitPoint.equals2D(arcs.back().p2)) {
+                        continue;
+                    }
+                    auto [a, b] = remainder.splitAtPoint(splitPoint);
+                    arcs.push_back(a);
+                    remainder = b;
+                }
+                arcs.push_back(remainder);
+            }
+        }
+
+        return std::make_unique<ArcString>(std::move(arcs));
+    }
+
+private:
+    std::map<size_t, std::vector<geom::CoordinateXYZM>> m_adds;
+};
+
+}
\ No newline at end of file
diff --git a/include/geos/noding/NodablePath.h b/include/geos/noding/NodablePath.h
new file mode 100644
index 000000000..35a4234c4
--- /dev/null
+++ b/include/geos/noding/NodablePath.h
@@ -0,0 +1,34 @@
+/**********************************************************************
+*
+ * GEOS - Geometry Engine Open Source
+ * http://geos.osgeo.org
+ *
+ * Copyright (C) 2025 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>
+
+namespace geos::noding {
+
+/// A NodablePath represents a PathString to which coordinates can be added.
+class GEOS_DLL NodablePath {
+    //virtual void addIntersection( const geom::Coordinate& intPt, int segmentIndex) =0;
+    //virtual void addIntersection( const geom::CoordinateXYM& intPt, int segmentIndex) =0;
+public:
+    virtual void addInt(const geom::CoordinateXY& intPt, size_t pathIndex) {
+        addIntersection(geom::CoordinateXYZM{intPt}, pathIndex);
+    }
+
+    virtual void addIntersection(geom::CoordinateXYZM intPt, size_t pathIndex) =0;
+};
+
+}
\ No newline at end of file
diff --git a/include/geos/noding/PathString.h b/include/geos/noding/PathString.h
new file mode 100644
index 000000000..e1a3ba4b3
--- /dev/null
+++ b/include/geos/noding/PathString.h
@@ -0,0 +1,35 @@
+/**********************************************************************
+*
+ * GEOS - Geometry Engine Open Source
+ * http://geos.osgeo.org
+ *
+ * Copyright (C) 2025 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 <cstddef>
+
+namespace geos::noding {
+
+/// A PathString represents a contiguous line/arc to the used as an input for
+/// noding. To access the coordinates, it is necessary to know whether they
+/// represent a set of line segments (SegmentString) or circular arcs (ArcString).
+class GEOS_DLL PathString {
+public:
+    virtual ~PathString() = default;
+
+    virtual std::size_t getSize() const = 0;
+
+    virtual double getLength() const = 0;
+};
+
+}
diff --git a/include/geos/noding/SegmentString.h b/include/geos/noding/SegmentString.h
index 5b76d8c89..d1aa26834 100644
--- a/include/geos/noding/SegmentString.h
+++ b/include/geos/noding/SegmentString.h
@@ -21,9 +21,11 @@
 #pragma once
 
 #include <geos/export.h>
+#include <geos/algorithm/Length.h>
 #include <geos/geom/Coordinate.h>
 #include <geos/geom/CoordinateSequence.h>
 #include <geos/noding/Octant.h>
+#include <geos/noding/PathString.h>
 
 #include <vector>
 
@@ -44,7 +46,7 @@ namespace noding { // geos.noding
  * SegmentStrings can carry a context object, which is useful
  * for preserving topological or parentage information.
  */
-class GEOS_DLL SegmentString {
+class GEOS_DLL SegmentString : public PathString {
 public:
     typedef std::vector<const SegmentString*> ConstVect;
     typedef std::vector<SegmentString*> NonConstVect;
@@ -89,14 +91,23 @@ public:
     }
 
     std::size_t size() const {
+        // FIXME: Remove this method, or make consistent with getSize
         return seq->size();
     }
 
+    std::size_t getSize() const override{
+        return seq->size() - 1;
+    }
+
     template<typename CoordType = geom::Coordinate>
     const CoordType& getCoordinate(std::size_t i) const {
         return seq->getAt<CoordType>(i);
     }
 
+    double getLength() const override {
+        return algorithm::Length::ofLine(seq.get());
+    }
+
     /// \brief
     /// Return a pointer to the CoordinateSequence associated
     /// with this SegmentString.
diff --git a/include/geos/noding/SimpleNoder.h b/include/geos/noding/SimpleNoder.h
index 70bc4765f..3be9796ec 100644
--- a/include/geos/noding/SimpleNoder.h
+++ b/include/geos/noding/SimpleNoder.h
@@ -22,22 +22,20 @@
 
 #include <vector>
 
-#include <geos/noding/SinglePassNoder.h>
-#include <geos/noding/NodedSegmentString.h> // for inlined (FIXME)
+#include <geos/noding/ArcNoder.h>
 
 // Forward declarations
 namespace geos {
 namespace noding {
-//class SegmentString;
+class PathString;
 }
 }
 
 namespace geos {
 namespace noding { // geos.noding
 
-
 /** \brief
- * Nodes a set of {@link SegmentString}s by
+ * Nodes a set of {@link SegmentString}s and/or {@link ArcString}s by
  * performing a brute-force comparison of every segment to every other one.
  *
  * This has n^2 performance, so is too slow for use on large numbers
@@ -45,24 +43,21 @@ namespace noding { // geos.noding
  *
  * @version 1.7
  */
-class GEOS_DLL SimpleNoder: public SinglePassNoder {
+class GEOS_DLL SimpleNoder: public ArcNoder {
 private:
-    std::vector<SegmentString*> nodedSegStrings;
-    void computeIntersects(SegmentString* e0, SegmentString* e1);
+    std::vector<PathString*> nodedSegStrings;
+    void computeIntersects(PathString& e0, PathString& e1);
 
 public:
-    SimpleNoder(SegmentIntersector* nSegInt = nullptr)
-        :
-        SinglePassNoder(nSegInt)
-    {}
 
-    void computeNodes(const std::vector<SegmentString*>& inputSegmentStrings) override;
+    SimpleNoder(std::unique_ptr<ArcIntersector>(nSegInt)) : ArcNoder(std::move(nSegInt)) {}
 
-    std::vector<std::unique_ptr<SegmentString>>
-    getNodedSubstrings() override
-    {
-        return NodedSegmentString::getNodedSubstrings(nodedSegStrings);
-    }
+    void computePathNodes(const std::vector<PathString*>& inputSegmentStrings) override;
+
+    std::vector<std::unique_ptr<PathString>> getNodedPaths() override;
+
+private:
+    std::vector<PathString*> m_pathStrings;
 };
 
 } // namespace geos.noding
diff --git a/src/algorithm/CircularArcIntersector.cpp b/src/algorithm/CircularArcIntersector.cpp
index 5ed2a44b7..2ebbd247d 100644
--- a/src/algorithm/CircularArcIntersector.cpp
+++ b/src/algorithm/CircularArcIntersector.cpp
@@ -99,6 +99,8 @@ CircularArcIntersector::intersects(const CircularArc& arc, const CoordinateXY& p
         return;
     }
 
+    reset();
+
     // TODO: envelope check?
     const CoordinateXY& c = arc.getCenter();
     const double r = arc.getRadius();
@@ -130,6 +132,8 @@ void
 CircularArcIntersector::intersects(const CoordinateXY& p0, const CoordinateXY& p1,
                                    const CoordinateXY& q0, const CoordinateXY& q1)
 {
+    reset();
+
     algorithm::LineIntersector li;
     li.computeIntersection(p0, p1, q0, q1);
     if (li.getIntersectionNum() == 2) {
@@ -163,6 +167,8 @@ CircularArcIntersector::intersects(const CircularArc& arc1, const CircularArc& a
         return;
     }
 
+    reset();
+
     const auto& c1 = arc1.getCenter();
     const auto& c2 = arc2.getCenter();
 
diff --git a/src/noding/ArcIntersectionAdder.cpp b/src/noding/ArcIntersectionAdder.cpp
new file mode 100644
index 000000000..278228436
--- /dev/null
+++ b/src/noding/ArcIntersectionAdder.cpp
@@ -0,0 +1,96 @@
+/**********************************************************************
+*
+ * GEOS - Geometry Engine Open Source
+ * http://geos.osgeo.org
+ *
+ * Copyright (C) 2025 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/noding/ArcIntersectionAdder.h>
+#include <geos/noding/NodableArcString.h>
+#include <geos/noding/NodedSegmentString.h>
+
+namespace geos::noding {
+
+void
+ArcIntersectionAdder::processIntersections(ArcString& e0, std::size_t segIndex0, ArcString& e1, std::size_t segIndex1)
+{
+    // don't bother intersecting a segment with itself
+    if(&e0 == &e1 && segIndex0 == segIndex1) {
+        return;
+    }
+
+    const geom::CircularArc& arc0 = e0.getArc(segIndex0);
+    const geom::CircularArc& arc1 = e1.getArc(segIndex0);
+
+    m_intersector.intersects(arc0, arc1);
+
+    if (m_intersector.getResult() == algorithm::CircularArcIntersector::NO_INTERSECTION) {
+        return;
+    }
+
+    // TODO handle cocircular intersections
+    for (std::uint8_t i = 0; i < m_intersector.getNumPoints(); i++) {
+        detail::down_cast<NodableArcString*>(&e0)->addIntersection(geom::CoordinateXYZM{m_intersector.getPoint(i)}, segIndex0);
+        detail::down_cast<NodableArcString*>(&e1)->addIntersection(geom::CoordinateXYZM{m_intersector.getPoint(i)}, segIndex1);
+    }
+}
+
+void
+ArcIntersectionAdder::processIntersections(ArcString& e0, std::size_t segIndex0, SegmentString& e1, std::size_t segIndex1)
+{
+// don't bother intersecting a segment with itself
+
+    const geom::CircularArc& arc = e0.getArc(segIndex0);
+    const geom::CoordinateXY& q0 = e1.getCoordinate(segIndex1);
+    const geom::CoordinateXY& q1 = e1.getCoordinate(segIndex1 + 1);
+
+    m_intersector.intersects(arc, q0, q1);
+
+    if (m_intersector.getResult() == algorithm::CircularArcIntersector::NO_INTERSECTION) {
+        return;
+    }
+
+    for (std::uint8_t i = 0; i < m_intersector.getNumPoints(); i++) {
+        detail::down_cast<NodableArcString*>(&e0)->addIntersection(geom::CoordinateXYZM{m_intersector.getPoint(i)}, segIndex0);
+        detail::down_cast<NodedSegmentString*>(&e1)->addIntersection(geom::CoordinateXYZM{m_intersector.getPoint(i)}, segIndex1);
+    }
+
+
+}
+
+void
+ArcIntersectionAdder::processIntersections(SegmentString& e0, std::size_t segIndex0, SegmentString& e1, std::size_t segIndex1)
+{
+    using geom::CoordinateXY;
+
+// don't bother intersecting a segment with itself
+    if(&e0 == &e1 && segIndex0 == segIndex1) {
+        return;
+    }
+
+    const CoordinateXY& p0 = e0.getCoordinate(segIndex0);
+    const CoordinateXY& p1 = e0.getCoordinate(segIndex0 + 1);
+    const CoordinateXY& q0 = e1.getCoordinate(segIndex1);
+    const CoordinateXY& q1 = e1.getCoordinate(segIndex1 + 1);
+
+    m_intersector.intersects(p0, p1, q0, q1);
+
+    if (m_intersector.getResult() == algorithm::CircularArcIntersector::NO_INTERSECTION) {
+        return;
+    }
+
+// todo collinear?
+
+    static_cast<NodedSegmentString&>(e0).addIntersection(m_intersector.getPoint(0), segIndex0);
+
+
+}
+
+}
\ No newline at end of file
diff --git a/src/noding/ArcNoder.cpp b/src/noding/ArcNoder.cpp
new file mode 100644
index 000000000..be26617c0
--- /dev/null
+++ b/src/noding/ArcNoder.cpp
@@ -0,0 +1,48 @@
+/**********************************************************************
+ *
+ * GEOS - Geometry Engine Open Source
+ * http://geos.osgeo.org
+ *
+ * Copyright (C) 2025 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/noding/ArcNoder.h>
+#include <geos/noding/PathString.h>
+#include <geos/noding/SegmentString.h>
+
+namespace geos::noding {
+
+ArcNoder::~ArcNoder() = default;
+
+void
+ArcNoder::computeNodes(const std::vector<SegmentString*>& segStrings)
+{
+    std::vector<PathString*> pathStrings(segStrings.size());
+    for (size_t i = 0; i < segStrings.size(); ++i) {
+        pathStrings[i] = segStrings[i];
+    }
+
+    computePathNodes(pathStrings);
+}
+
+std::vector<std::unique_ptr<SegmentString>>
+        ArcNoder::getNodedSubstrings()
+{
+    auto pathStrings(getNodedPaths());
+    std::vector<std::unique_ptr<SegmentString>> segStrings(pathStrings.size());
+    for (size_t i = 0; i < pathStrings.size(); ++i) {
+        // check inputs to verify they were actually linear, then set a flag that can be
+        // checked in this method?
+        segStrings[i].reset(detail::down_cast<SegmentString*>(pathStrings[i].release()));
+    }
+
+    return segStrings;
+}
+
+}
\ No newline at end of file
diff --git a/src/noding/SimpleNoder.cpp b/src/noding/SimpleNoder.cpp
index 6719a093e..7fc2007f3 100644
--- a/src/noding/SimpleNoder.cpp
+++ b/src/noding/SimpleNoder.cpp
@@ -19,44 +19,80 @@
 
 #include <geos/noding/SimpleNoder.h>
 #include <geos/noding/SegmentString.h>
-#include <geos/noding/SegmentIntersector.h>
 #include <geos/geom/CoordinateSequence.h>
+#include <geos/noding/ArcIntersector.h>
+#include <geos/noding/NodedSegmentString.h>
+#include <geos/noding/NodableArcString.h>
 
 using namespace geos::geom;
 
 namespace geos {
 namespace noding { // geos.noding
 
-/*private*/
-void
-SimpleNoder::computeIntersects(SegmentString* e0, SegmentString* e1)
+template<typename T1, typename T2>
+static void computeIntersectsImpl(ArcIntersector& intersector, T1& e0, T2& e1)
 {
-    assert(segInt); // must provide a segment intersector!
-
-    const CoordinateSequence* pts0 = e0->getCoordinates().get();
-    const CoordinateSequence* pts1 = e1->getCoordinates().get();
-    for(std::size_t i0 = 0, n0 = pts0->getSize() - 1; i0 < n0; i0++) {
-        for(std::size_t i1 = 0, n1 = pts1->getSize() - 1; i1 < n1; i1++) {
-            segInt->processIntersections(e0, i0, e1, i1);
+    for(std::size_t i = 0; i < e0.getSize(); i++) {
+        for(std::size_t j = 0; j < e1.getSize(); j++) {
+            intersector.processIntersections(e0, i, e1, j);
         }
     }
+}
 
+/*private*/
+void
+SimpleNoder::computeIntersects(PathString& e0, PathString& e1)
+{
+    assert(m_intersector); // must provide a segment intersector!
+
+    SegmentString* linear0 = dynamic_cast<SegmentString*>(&e0);
+    SegmentString* linear1 = dynamic_cast<SegmentString*>(&e1);
+
+    if (linear0 && linear1) {
+        computeIntersectsImpl(*m_intersector, *linear0, *linear1);
+    } else if (!linear0 && !linear1) {
+        computeIntersectsImpl(*m_intersector, *detail::down_cast<ArcString*>(&e0), *detail::down_cast<ArcString*>(&e1));
+    } else if (!linear0) {
+        computeIntersectsImpl(*m_intersector, *detail::down_cast<ArcString*>(&e0), *linear1);
+    } else {
+        computeIntersectsImpl(*m_intersector, *linear0, *detail::down_cast<ArcString*>(&e1));
+    }
 }
 
 /*public*/
 void
-SimpleNoder::computeNodes(const std::vector<SegmentString*>& inputSegmentStrings)
+SimpleNoder::computePathNodes(const std::vector<PathString*>& inputPathStrings)
 {
-    nodedSegStrings = inputSegmentStrings;
+    m_pathStrings = inputPathStrings;
 
-    for (SegmentString* edge0: inputSegmentStrings) {
-        for (SegmentString* edge1: inputSegmentStrings) {
-            computeIntersects(edge0, edge1);
+    for (auto* edge0: m_pathStrings) {
+        for (auto* edge1: m_pathStrings) {
+            // TODO skip processing against self?
+            computeIntersects(*edge0, *edge1);
+        }
+    }
+}
+
+std::vector<std::unique_ptr<PathString>>
+SimpleNoder::getNodedPaths()
+{
+    std::vector<std::unique_ptr<PathString>> nodedPaths;
+
+    for (PathString* ps : m_pathStrings) {
+        if (auto* nss = dynamic_cast<NodedSegmentString*>(ps)) {
+            std::vector<std::unique_ptr<SegmentString>> tmp;
+            nss->getNodeList().addSplitEdges(tmp);
+            for (auto& segString : tmp) {
+                nodedPaths.push_back(std::move(segString));
+            }
+        } else {
+            auto* nas = detail::down_cast<NodableArcString*>(ps);
+            nodedPaths.push_back(nas->getNoded());
         }
     }
 
+    return nodedPaths;
 }
 
-
 } // namespace geos.noding
 } // namespace geos
diff --git a/tests/unit/noding/NodableArcStringTest.cpp b/tests/unit/noding/NodableArcStringTest.cpp
new file mode 100644
index 000000000..bde0f4970
--- /dev/null
+++ b/tests/unit/noding/NodableArcStringTest.cpp
@@ -0,0 +1,111 @@
+#include <tut/tut.hpp>
+#include "utility.h"
+
+#include <geos/noding/NodableArcString.h>
+
+using geos::algorithm::Orientation;
+using geos::geom::CircularArc;
+using geos::geom::CoordinateXY;
+using geos::noding::NodableArcString;
+
+namespace tut {
+
+struct test_nodablearcstring_data {
+
+    static void test_add_points(const CircularArc& arc, const std::vector<CoordinateXY>& coords,
+                                const std::vector<CircularArc>& expected, bool reversed=false) {
+        std::vector<CircularArc> arcs;
+        arcs.push_back(arc);
+        NodableArcString nas(arcs);
+
+        for (const auto& coord : coords) {
+            nas.addInt(coord, 0);
+        }
+
+        auto noded = nas.getNoded();
+
+        ensure_equals(noded->getSize(), expected.size());
+
+        for (std::size_t i = 0; i < expected.size(); i++) {
+            ensure_arc_equals(noded->getArc(i), expected[i]);
+        }
+
+        if (!reversed) {
+            auto revArc = arc;
+            revArc.reverse();
+
+            auto revExpected = expected;
+            for (auto& x : revExpected) {
+                x.reverse();
+            }
+            std::reverse(revExpected.begin(), revExpected.end());
+
+            test_add_points(revArc, coords, revExpected, true);
+        }
+    }
+
+    static void ensure_arc_equals(const CircularArc& actual, const CircularArc& expected) {
+        ensure_equals_xy(actual.p0, expected.p0);
+        ensure_equals_xy(actual.p2, expected.p2);
+        ensure_equals_xy(actual.getCenter(), expected.getCenter());
+        ensure_equals(actual.getRadius(), expected.getRadius());
+        ensure_equals(actual.getOrientation(), expected.getOrientation());
+    }
+};
+
+typedef test_group<test_nodablearcstring_data> group;
+typedef group::object object;
+
+group test_nodablearcstring_group("geos::noding::NodableArcString");
+
+template<>
+template<>
+void object::test<1>()
+{
+    set_test_name("CW half-circle, upper half-plane");
+
+    CircularArc in(CoordinateXY{-5, 0}, CoordinateXY{0, 5}, CoordinateXY{5, 0});
+
+    std::vector<CoordinateXY> coords;
+    coords.emplace_back(4, 3);
+    coords.emplace_back(3, 4);
+    coords.emplace_back(-3, 4);
+    coords.emplace_back(-4, 3);
+
+    std::vector<CircularArc> expected;
+    expected.push_back(CircularArc({-5, 0}, {-4, 3}, {0, 0}, 5, Orientation::CLOCKWISE));
+    expected.push_back(CircularArc({-4, 3}, {-3, 4}, {0, 0}, 5, Orientation::CLOCKWISE));
+    expected.push_back(CircularArc({-3, 4}, {3, 4}, {0, 0}, 5, Orientation::CLOCKWISE));
+    expected.push_back(CircularArc({3, 4}, {4, 3}, {0, 0}, 5, Orientation::CLOCKWISE));
+    expected.push_back(CircularArc({4, 3}, {5, 0}, {0, 0}, 5, Orientation::CLOCKWISE));
+
+    test_add_points(in, coords, expected);
+}
+
+template<>
+template<>
+void object::test<2>()
+{
+    set_test_name("CW half-circle, right half-plane");
+
+    CircularArc in(CoordinateXY{0, 5}, CoordinateXY{5, 0}, CoordinateXY{0, -5});
+
+    std::vector<CoordinateXY> coords;
+    coords.emplace_back(4, -3);
+    coords.emplace_back(4, 3);
+    coords.emplace_back(3, -4);
+    coords.emplace_back(3, 4);
+    coords.emplace_back(5, 0);
+
+    std::vector<CircularArc> expected;
+    expected.push_back(CircularArc({0, 5}, {3, 4}, {0, 0}, 5, Orientation::CLOCKWISE));
+    expected.push_back(CircularArc({3, 4}, {4, 3}, {0, 0}, 5, Orientation::CLOCKWISE));
+    expected.push_back(CircularArc({4, 3}, {5, 0}, {0, 0}, 5, Orientation::CLOCKWISE));
+    expected.push_back(CircularArc({5, 0}, {4, -3}, {0, 0}, 5, Orientation::CLOCKWISE));
+    expected.push_back(CircularArc({4, -3}, {3, -4}, {0, 0}, 5, Orientation::CLOCKWISE));
+    expected.push_back(CircularArc({3, -4}, {0, -5}, {0, 0}, 5, Orientation::CLOCKWISE));
+
+    test_add_points(in, coords, expected);
+}
+
+}
\ No newline at end of file
diff --git a/tests/unit/noding/SimpleNoderTest.cpp b/tests/unit/noding/SimpleNoderTest.cpp
new file mode 100644
index 000000000..7450724c6
--- /dev/null
+++ b/tests/unit/noding/SimpleNoderTest.cpp
@@ -0,0 +1,171 @@
+#include <tut/tut.hpp>
+
+#include <geos/algorithm/Orientation.h>
+#include <geos/geom/Coordinate.h>
+#include <geos/geom/CoordinateSequence.h>
+#include <geos/noding/ArcIntersectionAdder.h>
+#include <geos/noding/NodableArcString.h>
+#include <geos/noding/NodedSegmentString.h>
+#include <geos/noding/SegmentString.h>
+#include <geos/noding/SimpleNoder.h>
+
+using geos::geom::CoordinateXY;
+using geos::geom::CoordinateSequence;
+using geos::geom::CircularArc;
+using geos::algorithm::Orientation;
+using geos::noding::ArcString;
+using geos::noding::SegmentString;
+using geos::noding::NodableArcString;
+using geos::noding::NodedSegmentString;
+using geos::noding::PathString;
+using geos::noding::SimpleNoder;
+
+namespace tut {
+
+struct test_simplenoder_data {
+
+    template<typename T1, typename T2>
+    static void
+    check_length_equal(const T1& actual, const T2& expected) {
+        double tot_actual = 0.0;
+        double tot_expected = 0.0;
+
+        for (const auto& path : actual) {
+            tot_actual += path->getLength();
+        }
+        for (const auto& path : expected) {
+            tot_expected += path->getLength();
+        }
+
+        ensure_equals("length does not match expected", tot_actual, tot_expected, 1e-8);
+    }
+
+    template<typename T>
+    static void printPaths(const T& paths) {
+        std::cout << "GEOMETRYCOLLECTION (";
+
+        bool collFirst = true;
+
+        for (const auto& path : paths) {
+            if (collFirst) {
+                collFirst = false;
+            } else {
+                std::cout << ", ";
+            }
+
+            if (auto* segString = dynamic_cast<const SegmentString*>(&*path)) {
+                std::cout << "LINESTRING (";
+
+                bool first = true;
+                const CoordinateSequence& seq = *segString->getCoordinates();
+                seq.forEach([&first](const auto& pt) {
+                    if (first) {
+                        first = false;
+                    } else {
+                        std::cout << ", ";
+                    }
+
+                    std::cout << pt;
+                });
+
+                std::cout << ")";
+            } else {
+                auto* arcString = static_cast<const ArcString*>(&*path);
+                std::cout << "CIRCULARSTRING (";
+                bool first = true;
+                for (const auto& arc : *arcString) {
+                    if (first) {
+                        first = false;
+                        std::cout << arc.p0 << ", ";
+                    } else {
+                        std::cout << ", ";
+                    }
+                    std::cout << arc.p1 << ", " << arc.p2;
+                }
+                std::cout << ")";
+            }
+
+        }
+
+        std::cout << ")";
+    }
+};
+
+typedef test_group<test_simplenoder_data> group;
+typedef group::object object;
+
+group test_simplenoder_group("geos::noding::SimpleNoder");
+
+template<>
+template<>
+void object::test<1>()
+{
+    set_test_name("segment-segment intersection");
+
+    auto seq1 = std::make_shared<CoordinateSequence>();
+    seq1->add(CoordinateXY{0, 0});
+    seq1->add(CoordinateXY{1, 1});
+    NodedSegmentString ss1(seq1, false, false, nullptr);
+
+    auto seq2 = std::make_shared<CoordinateSequence>();
+    seq2->add(CoordinateXY{1, 0});
+    seq2->add(CoordinateXY{0, 1});
+    NodedSegmentString ss2(seq2, false, false, nullptr);
+
+    std::vector<PathString*> ss{&ss1, &ss2};
+
+    SimpleNoder noder(std::make_unique<geos::noding::ArcIntersectionAdder>());
+    noder.computePathNodes(ss);
+
+    auto paths = noder.getNodedPaths();
+
+    check_length_equal(paths, ss);
+}
+
+template<>
+template<>
+void object::test<2>()
+{
+    set_test_name("arc-arc intersection");
+
+    CircularArc arc0({-1, 0}, {1, 0}, {0, 0}, 1, Orientation::CLOCKWISE);
+    CircularArc arc1({-1, 1}, {1, 1}, {0, 1}, 1, Orientation::COUNTERCLOCKWISE);
+
+    NodableArcString as0({arc0});
+    NodableArcString as1({arc1});
+
+    std::vector<PathString*> ss{&as0, &as1};
+
+    SimpleNoder noder(std::make_unique<geos::noding::ArcIntersectionAdder>());
+    noder.computePathNodes(ss);
+
+    auto paths = noder.getNodedPaths();
+
+    check_length_equal(paths, ss);
+}
+
+template<>
+template<>
+void object::test<3>()
+{
+    set_test_name("arc-segment intersection");
+
+    CircularArc arc0({-1, 0}, {1, 0}, {0, 0}, 1, Orientation::CLOCKWISE);
+    NodableArcString as0({arc0});
+
+    auto seq1 = std::make_shared<CoordinateSequence>();
+    seq1->add(CoordinateXY{-1, 0.5});
+    seq1->add(CoordinateXY{1, 0.5});
+    NodedSegmentString ss1(seq1, false, false, nullptr);
+
+    std::vector<PathString*> ss{&as0, &ss1};
+
+    SimpleNoder noder(std::make_unique<geos::noding::ArcIntersectionAdder>());
+    noder.computePathNodes(ss);
+
+    auto paths = noder.getNodedPaths();
+
+    check_length_equal(paths, ss);
+}
+
+}

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

Summary of changes:
 include/geos/algorithm/Angle.h                     |   19 +
 include/geos/algorithm/CircularArcIntersector.h    |  104 +-
 include/geos/algorithm/Interpolate.h               |   19 +-
 include/geos/algorithm/LineIntersector.h           |    6 +-
 include/geos/algorithm/RayCrossingCounter.h        |    4 +-
 include/geos/geom/CircularArc.h                    |  311 ++---
 include/geos/geom/CircularString.h                 |    8 +
 include/geos/geom/Coordinate.h                     |  108 ++
 include/geos/geom/CoordinateSequence.h             |    2 +
 include/geos/noding/ArcIntersectionAdder.h         |   39 +
 include/geos/noding/ArcIntersector.h               |   44 +
 include/geos/noding/ArcNoder.h                     |   53 +
 include/geos/noding/ArcString.h                    |   73 ++
 include/geos/noding/GeometryNoder.h                |   11 +-
 include/geos/noding/NodableArcString.h             |   44 +
 include/geos/noding/NodablePath.h                  |   40 +
 include/geos/noding/PathString.h                   |   48 +
 include/geos/noding/SegmentString.h                |   17 +-
 include/geos/noding/SimpleNoder.h                  |   31 +-
 src/algorithm/Angle.cpp                            |   23 +
 src/algorithm/Area.cpp                             |   14 +-
 src/algorithm/CircularArcIntersector.cpp           |  577 +++++++--
 src/algorithm/LineIntersector.cpp                  |   46 +-
 src/algorithm/RayCrossingCounter.cpp               |   22 +-
 src/geom/CircularArc.cpp                           |  405 ++++++
 src/geom/CircularString.cpp                        |   25 +-
 src/geom/CoordinateSequence.cpp                    |   12 +
 src/noding/ArcIntersectionAdder.cpp                |  100 ++
 src/noding/ArcNoder.cpp                            |   48 +
 .../{SegmentStringUtil.cpp => ArcString.cpp}       |   19 +-
 src/noding/GeometryNoder.cpp                       |  114 +-
 src/noding/NodableArcString.cpp                    |  131 ++
 .../UnionStrategy.cpp => noding/PathString.cpp}    |   25 +-
 src/noding/SegmentString.cpp                       |    9 +
 src/noding/SimpleNoder.cpp                         |   67 +-
 tests/unit/algorithm/AngleTest.cpp                 |   24 +
 .../unit/algorithm/CircularArcIntersectorTest.cpp  | 1347 ++++++++++++++++----
 tests/unit/algorithm/CircularArcsTest.cpp          |   42 +-
 .../unit/algorithm/RobustLineIntersectorZTest.cpp  |    2 +-
 tests/unit/capi/GEOSNodeTest.cpp                   |  149 ++-
 tests/unit/geom/CircularArcTest.cpp                |   30 +-
 tests/unit/geom/CoordinateSequenceTest.cpp         |   25 +
 tests/unit/geom/CoordinateTest.cpp                 |   26 +
 tests/unit/noding/NodableArcStringTest.cpp         |  164 +++
 tests/unit/noding/SimpleNoderTest.cpp              |  189 +++
 tests/unit/utility.h                               |   74 +-
 46 files changed, 3901 insertions(+), 789 deletions(-)
 create mode 100644 include/geos/noding/ArcIntersectionAdder.h
 create mode 100644 include/geos/noding/ArcIntersector.h
 create mode 100644 include/geos/noding/ArcNoder.h
 create mode 100644 include/geos/noding/ArcString.h
 create mode 100644 include/geos/noding/NodableArcString.h
 create mode 100644 include/geos/noding/NodablePath.h
 create mode 100644 include/geos/noding/PathString.h
 create mode 100644 src/geom/CircularArc.cpp
 create mode 100644 src/noding/ArcIntersectionAdder.cpp
 create mode 100644 src/noding/ArcNoder.cpp
 copy src/noding/{SegmentStringUtil.cpp => ArcString.cpp} (67%)
 create mode 100644 src/noding/NodableArcString.cpp
 copy src/{operation/union/UnionStrategy.cpp => noding/PathString.cpp} (51%)
 create mode 100644 tests/unit/noding/NodableArcStringTest.cpp
 create mode 100644 tests/unit/noding/SimpleNoderTest.cpp


hooks/post-receive
-- 
GEOS


More information about the geos-commits mailing list