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

git at osgeo.org git at osgeo.org
Tue Aug 13 09:16:17 PDT 2024


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

The branch, main has been updated
       via  7ed90dfd7f1c27acd2f86574e5d01effd27f6d8e (commit)
       via  d963c0f0becb83979f732e940f2f8eb80584a5a8 (commit)
      from  85b06abbace70f47c699b081be951930edfd1d50 (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 7ed90dfd7f1c27acd2f86574e5d01effd27f6d8e
Author: Paul Ramsey <pramsey at cleverelephant.ca>
Date:   Tue Aug 13 09:15:34 2024 -0700

    News entry for RelateNG port

diff --git a/NEWS.md b/NEWS.md
index 02b748e0a..45129ef8c 100644
--- a/NEWS.md
+++ b/NEWS.md
@@ -6,6 +6,15 @@
   - Add classes for curved geometry types: CircularString, CompoundCurve, CurvedPolygon, MultiCurve,
     MultiSurface (GH-1046, Dan Baston/German QGIS users group/Canton of Basel-Landschaft/Canton of Zug)
   - Support curved geometry types in WKT/WKB readers/writers (GH-1046, GH-1104, GH-1106, Dan Baston)
+  - Port of RelateNG https://github.com/locationtech/jts/pull/1052 (Martin Davis, Paul Ramsey)
+    - rewrite of boolean predicates and relate matrix calculations. 
+    - "Prepared" mode now available for all predicates and relate matrix
+    - CAPI functions GEOSPreparedRelate and GEOSPreparedRelatePattern expose new functionality
+    - CAPI implementations of GEOSPreparedTouches, etc, that were previously defaulting 
+      into non-prepared implementations now default into the RelateNG prepared implementation
+    - Prepared implementations for Intersects, Covers, still use the older implementations
+    - https://lin-ear-th-inking.blogspot.com/2024/05/jts-topological-relationships-next.html
+    - https://lin-ear-th-inking.blogspot.com/2024/05/relateng-performance.html 
 
 - Breaking Changes
 

commit d963c0f0becb83979f732e940f2f8eb80584a5a8
Author: Paul Ramsey <pramsey at cleverelephant.ca>
Date:   Tue Aug 13 09:12:51 2024 -0700

    Port of JTS [RelateNG](https://github.com/locationtech/jts/pull/1052), rewrite of boolean predicates and relate matrix calculations.
    
    * No new functions, just rework of existing functionality
    * "Prepared" mode now available for all predicates and relate matrix
    * CAPI functions GEOSPreparedRelate and GEOSPreparedRelatePattern expose new functionality
    * CAPI implementations of GEOSPreparedTouches, etc, that were previously defaulting into non-prepared implementations now default into the RelateNG prepared implementation
    * Prepared implementations for Intersects, Covers, still use the older implementations
    * https://lin-ear-th-inking.blogspot.com/2024/05/jts-topological-relationships-next.html
    * https://lin-ear-th-inking.blogspot.com/2024/05/relateng-performance.html

diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index d657513d8..bf0ccab67 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -273,7 +273,7 @@ jobs:
       working-directory: ./build
       run: |
         ctest --output-on-failure \
-          --overwrite MemoryCheckCommandOptions="--leak-check=full --error-exitcode=100" \
+          --overwrite MemoryCheckCommandOptions="--leak-check=full --error-exitcode=100 --num-callers=100" \
           -R ^all-unit -C Valgrind -T memcheck
 
     - name: 'Upload Valgrind Log'
diff --git a/benchmarks/BenchmarkUtils.h b/benchmarks/BenchmarkUtils.h
index c3780d51a..5793e4c4e 100644
--- a/benchmarks/BenchmarkUtils.h
+++ b/benchmarks/BenchmarkUtils.h
@@ -58,6 +58,12 @@ createLine(const geom::CoordinateXY& base, double size, std::size_t npts) {
 }
 
 
+std::vector<std::unique_ptr<geom::Geometry>>
+createPolygons(const geom::Envelope& env, std::size_t nItems, double size, std::size_t npts) {
+    return createGeometriesOnGrid(env, nItems, [size, npts](const geom::CoordinateXY& base) {
+        return createSineStar(base, size, npts);
+    });
+}
 std::vector<std::unique_ptr<geom::Geometry>>
 createLines(const geom::Envelope& env, std::size_t nItems, double size, std::size_t npts) {
     return createGeometriesOnGrid(env, nItems, [size, npts](const geom::CoordinateXY& base) {
diff --git a/benchmarks/geom/CMakeLists.txt b/benchmarks/geom/CMakeLists.txt
index 63a25ed05..2d68291f7 100644
--- a/benchmarks/geom/CMakeLists.txt
+++ b/benchmarks/geom/CMakeLists.txt
@@ -32,8 +32,8 @@ IF(benchmark_FOUND)
             benchmark::benchmark geos_cxx_flags)
 endif()
 
-add_executable(perf_prepared_polygon_intersects
-    PreparedPolygonIntersectsPerfTest.cpp)
-target_include_directories(perf_prepared_polygon_intersects PUBLIC
+add_executable(perf_topo_predicate
+    TopologyPredicatePerfTest.cpp)
+target_include_directories(perf_topo_predicate PUBLIC
     $<BUILD_INTERFACE:${PROJECT_SOURCE_DIR}/benchmarks>)
-target_link_libraries(perf_prepared_polygon_intersects PRIVATE geos)
+target_link_libraries(perf_topo_predicate PRIVATE geos)
diff --git a/benchmarks/geom/PreparedPolygonIntersectsPerfTest.cpp b/benchmarks/geom/PreparedPolygonIntersectsPerfTest.cpp
deleted file mode 100644
index 0432cecaf..000000000
--- a/benchmarks/geom/PreparedPolygonIntersectsPerfTest.cpp
+++ /dev/null
@@ -1,89 +0,0 @@
-#include <geos/geom/util/SineStarFactory.h>
-#include <geos/geom/GeometryFactory.h>
-#include <geos/profiler.h>
-#include <geos/geom/IntersectionMatrix.h>
-#include <geos/geom/prep/PreparedGeometryFactory.h>
-#include <geos/io/WKBWriter.h>
-#include <BenchmarkUtils.h>
-
-using namespace geos::geom;
-
-std::size_t MAX_ITER = 10;
-std::size_t NUM_LINES = 10000;
-std::size_t NUM_LINES_PTS = 100;
-
-
-int testRelateOp(const Geometry& g, const std::vector<std::unique_ptr<Geometry>>& lines) {
-    int count = 0;
-    for (const auto& line : lines) {
-        auto im = g.relate(line.get());
-        count += im->isIntersects();
-    }
-    return count;
-}
-
-int testGeometryIntersects(const Geometry& g, const std::vector<std::unique_ptr<Geometry>>& lines) {
-    int count = 0;
-    for (const auto& line : lines) {
-        count += g.intersects(line.get());
-    }
-    return count;
-}
-
-int testGeometryContains(const Geometry& g, const std::vector<std::unique_ptr<Geometry>>& lines) {
-    int count = 0;
-    for (const auto& line : lines) {
-        count += g.contains(line.get());
-    }
-    return count;
-}
-
-int testPrepGeomCached(const Geometry& g, const std::vector<std::unique_ptr<Geometry>>& lines) {
-    int count = 0;
-    auto prep = prep::PreparedGeometryFactory::prepare(&g);
-    for (const auto& line : lines) {
-        count += prep->intersects(line.get());
-    }
-    return count;
-}
-
-template<typename F>
-void test(const Geometry& g, const std::vector<std::unique_ptr<Geometry>>& lines, const std::string& method, F&& fun)
-{
-    geos::util::Profile sw("PreparedPolygonIntersects");
-    sw.start();
-
-    int count = 0;
-    for (std::size_t i = 0; i < MAX_ITER; i++) {
-        count += fun(g, lines);
-    }
-
-    sw.stop();
-    std::cout << g.getNumPoints() << "," << MAX_ITER * lines.size() << "," << count << "," << lines[0]->getGeometryType() << "," << lines[0]->getNumPoints() << "," << method << "," << sw.getTot() << std::endl;
-}
-
-void test (std::size_t npts) {
-
-    auto target = geos::benchmark::createSineStar({0, 0}, 100, npts);
-    auto lines = geos::benchmark::createLines(*target->getEnvelopeInternal(), NUM_LINES, 1.0, NUM_LINES_PTS);
-    auto points = geos::benchmark::createPoints(*target->getEnvelopeInternal(), NUM_LINES);
-
-    test(*target, lines, "RelateOp", testRelateOp);
-    test(*target, lines, "Geometry::intersects", testGeometryIntersects);
-    test(*target, lines, "PrepGeomCached", testPrepGeomCached);
-    test(*target, points, "RelateOp", testRelateOp);
-    test(*target, points, "Geometry::intersects", testGeometryIntersects);
-    test(*target, points, "PrepGeomCached", testPrepGeomCached);
-}
-
-int main() {
-    std::cout << "target_points,num_tests,num_hits,test_type,pts_in_test,method,time" << std::endl;
-    test(5);
-    test(10);
-    test(500);
-    test(1000);
-    test(2000);
-    test(4000);
-    test(8000);
-    test(16000);
-}
diff --git a/benchmarks/geom/TopologyPredicatePerfTest.cpp b/benchmarks/geom/TopologyPredicatePerfTest.cpp
new file mode 100644
index 000000000..05c9ae694
--- /dev/null
+++ b/benchmarks/geom/TopologyPredicatePerfTest.cpp
@@ -0,0 +1,266 @@
+/******************************************************
+ *   Performance tests for topological predicates
+ * 
+ * Usage: perf_topo_predicate [ intersects | contains ]
+******************************************************/
+
+#include <geos/geom/util/SineStarFactory.h>
+#include <geos/geom/GeometryFactory.h>
+#include <geos/profiler.h>
+#include <geos/geom/IntersectionMatrix.h>
+#include <geos/geom/prep/PreparedGeometryFactory.h>
+#include <geos/operation/relateng/RelateNG.h>
+#include <geos/operation/relateng/RelatePredicate.h>
+#include <geos/io/WKBWriter.h>
+#include <BenchmarkUtils.h>
+
+#include <iomanip>
+
+using namespace geos::geom;
+
+std::size_t MAX_ITER = 10;
+std::size_t NUM_LINES = 10000;
+std::size_t NUM_LINES_PTS = 100;
+
+#define INTERSECTS 0
+#define CONTAINS 1
+#define COVERS 2
+#define TOUCHES 3
+
+std::string predicateName{"intersects"};
+int predicateOp = INTERSECTS;
+
+int testRelateOpIntersects(const Geometry& g, const std::vector<std::unique_ptr<Geometry>>& geoms) {
+    int count = 0;
+    for (const auto& geom : geoms) {
+        auto im = g.relate(geom.get());
+        count += im->isIntersects();
+    }
+    return count;
+}
+
+int testRelateOpContains(const Geometry& g, const std::vector<std::unique_ptr<Geometry>>& geoms) {
+    int count = 0;
+    for (const auto& geom : geoms) {
+        auto im = g.relate(geom.get());
+        count += im->isContains();
+    }
+    return count;
+}
+
+int testRelateOpCovers(const Geometry& g, const std::vector<std::unique_ptr<Geometry>>& geoms) {
+    int count = 0;
+    for (const auto& geom : geoms) {
+        auto im = g.relate(geom.get());
+        count += im->isCovers();
+    }
+    return count;
+}
+
+int testRelateOpTouches(const Geometry& g, const std::vector<std::unique_ptr<Geometry>>& geoms) {
+    int count = 0;
+    for (const auto& geom : geoms) {
+        auto im = g.relate(geom.get());
+        count += im->isTouches(g.getDimension(), geom.get()->getDimension());
+    }
+    return count;
+}
+
+int testGeometryIntersects(const Geometry& g, const std::vector<std::unique_ptr<Geometry>>& geoms) {
+    int count = 0;
+    for (const auto& geom : geoms) {
+        count += g.intersects(geom.get());
+    }
+    return count;
+}
+
+int testGeometryContains(const Geometry& g, const std::vector<std::unique_ptr<Geometry>>& geoms) {
+    int count = 0;
+    for (const auto& geom : geoms) {
+        count += g.contains(geom.get());
+    }
+    return count;
+}
+
+int testGeometryCovers(const Geometry& g, const std::vector<std::unique_ptr<Geometry>>& geoms) {
+    int count = 0;
+    for (const auto& geom : geoms) {
+        count += g.covers(geom.get());
+    }
+    return count;
+}
+
+int testGeometryTouches(const Geometry& g, const std::vector<std::unique_ptr<Geometry>>& geoms) {
+    int count = 0;
+    for (const auto& geom : geoms) {
+        count += g.touches(geom.get());
+    }
+    return count;
+}
+
+int testPrepGeomIntersects(const Geometry& g, const std::vector<std::unique_ptr<Geometry>>& geoms) {
+    int count = 0;
+    auto prep = prep::PreparedGeometryFactory::prepare(&g);
+    for (const auto& geom : geoms) {
+        count += prep->intersects(geom.get());
+    }
+    return count;
+}
+
+int testPrepGeomContains(const Geometry& g, const std::vector<std::unique_ptr<Geometry>>& geoms) {
+    int count = 0;
+    auto prep = prep::PreparedGeometryFactory::prepare(&g);
+    for (const auto& geom : geoms) {
+        count += prep->contains(geom.get());
+    }
+    return count;
+}
+
+int testPrepGeomCovers(const Geometry& g, const std::vector<std::unique_ptr<Geometry>>& geoms) {
+    int count = 0;
+    auto prep = prep::PreparedGeometryFactory::prepare(&g);
+    for (const auto& geom : geoms) {
+        count += prep->covers(geom.get());
+    }
+    return count;
+}
+
+int testRelateNGPreparedIntersects(const Geometry& g, const std::vector<std::unique_ptr<Geometry>>& geoms) {
+    int count = 0;
+    auto prep = geos::operation::relateng::RelateNG::prepare(&g);
+    for (const auto& line : geoms) {
+        count += prep->evaluate(line.get(), *geos::operation::relateng::RelatePredicate::intersects());
+    }
+    return count;
+}
+
+int testRelateNGPreparedContains(const Geometry& g, const std::vector<std::unique_ptr<Geometry>>& geoms) {
+    int count = 0;
+    auto prep = geos::operation::relateng::RelateNG::prepare(&g);
+    for (const auto& line : geoms) {
+        count += prep->evaluate(line.get(), *geos::operation::relateng::RelatePredicate::contains());
+    }
+    return count;
+}
+
+int testRelateNGPreparedCovers(const Geometry& g, const std::vector<std::unique_ptr<Geometry>>& geoms) {
+    int count = 0;
+    auto prep = geos::operation::relateng::RelateNG::prepare(&g);
+    for (const auto& line : geoms) {
+        count += prep->evaluate(line.get(), *geos::operation::relateng::RelatePredicate::covers());
+    }
+    return count;
+}
+
+int testRelateNGPreparedTouches(const Geometry& g, const std::vector<std::unique_ptr<Geometry>>& geoms) {
+    int count = 0;
+    auto prep = geos::operation::relateng::RelateNG::prepare(&g);
+    for (const auto& line : geoms) {
+        count += prep->evaluate(line.get(), *geos::operation::relateng::RelatePredicate::touches());
+    }
+    return count;
+}
+
+template<typename F>
+double test(const Geometry& g, const std::vector<std::unique_ptr<Geometry>>& geoms, const std::string& method, F&& fun, double baseTime)
+{
+    geos::util::Profile sw("TopologuyPredicatePerf");
+    sw.start();
+
+    int count = 0;
+    for (std::size_t i = 0; i < MAX_ITER; i++) {
+        count += fun(g, geoms);
+    }
+
+    sw.stop();
+    double tot = sw.getTot();
+    double timesFaster = baseTime == 0 ? 1 : baseTime / tot;
+    std::cout << std::fixed << std::setprecision(0);
+    std::cout << g.getNumPoints() << "," 
+        << MAX_ITER * geoms.size() << "," 
+        << count << "," << geoms[0]->getGeometryType() << "," 
+        << geoms[0]->getNumPoints() << "," 
+        << method << " - " << predicateName << "," 
+        << tot << ",";
+    std::cout << std::fixed << std::setprecision(1);
+    std::cout << timesFaster 
+        << std::endl;
+    return tot;
+}
+
+void test(int dim, std::size_t npts) {
+
+    auto target = geos::benchmark::createSineStar({0, 0}, 100, npts);
+    std::vector<std::unique_ptr<Geometry>> geoms;
+    switch (dim) {
+    case 0:
+        geoms = geos::benchmark::createPoints(*target->getEnvelopeInternal(), NUM_LINES);
+        break;
+    case 1:
+        geoms = geos::benchmark::createLines(*target->getEnvelopeInternal(), NUM_LINES, 1.0, NUM_LINES_PTS);
+        break;
+    case 2:
+        geoms = geos::benchmark::createPolygons(*target->getEnvelopeInternal(), NUM_LINES, 1.0, NUM_LINES_PTS);
+        break;
+    }
+    double baseTime;
+    switch (predicateOp) {
+    case INTERSECTS:
+        baseTime = test(*target, geoms, "RelateOp", testRelateOpIntersects, 0);
+        test(*target, geoms, "Geometry", testGeometryIntersects, baseTime);
+        test(*target, geoms, "PreparedGeom", testPrepGeomIntersects, baseTime);
+        test(*target, geoms, "RelateNGPrepared", testRelateNGPreparedIntersects, baseTime);
+        break;
+    case CONTAINS:
+        baseTime = test(*target, geoms, "RelateOp", testRelateOpIntersects, 0);
+        test(*target, geoms, "Geometry", testGeometryIntersects, baseTime);
+        test(*target, geoms, "PreparedGeom", testPrepGeomContains, baseTime);
+        test(*target, geoms, "RelateNGPrepared", testRelateNGPreparedContains, baseTime);
+        break;
+    case COVERS:
+        baseTime = test(*target, geoms, "RelateOp", testRelateOpCovers, 0);
+        test(*target, geoms, "Geometry", testGeometryCovers, baseTime);
+        test(*target, geoms, "PreparedGeom", testPrepGeomCovers, baseTime);
+        test(*target, geoms, "RelateNGPrepared", testRelateNGPreparedCovers, baseTime);
+        break;
+    case TOUCHES:
+        baseTime = test(*target, geoms, "RelateOp", testRelateOpTouches, 0);
+        test(*target, geoms, "Geometry", testGeometryTouches, baseTime);
+        test(*target, geoms, "RelateNGPrepared", testRelateNGPreparedTouches, baseTime);
+        break;
+    }
+}
+
+void testAll(int dim)
+{
+    test(dim, 5);
+    test(dim, 10);
+    test(dim, 500);
+    test(dim, 1000);
+    test(dim, 2000);
+    test(dim, 4000);
+    test(dim, 8000);
+    test(dim, 16000);
+}
+
+int main(int argc, char** argv) {
+    predicateName = "intersects";
+    if (argc >= 2) {
+        predicateName = argv[1];
+    }
+    predicateOp = INTERSECTS;
+    if (predicateName == "contains") {
+        predicateOp = CONTAINS;
+    }
+    else if (predicateName == "covers") {
+        predicateOp = COVERS;
+    }
+    else if (predicateName == "touches") {
+        predicateOp = TOUCHES;
+    }
+
+    std::cout << "target_points,num_tests,num_hits,test_type,pts_in_test,method,time,factor" << std::endl;
+    testAll(0);
+    testAll(1);
+    testAll(2);
+}
diff --git a/capi/geos_c.cpp b/capi/geos_c.cpp
index 98025b7a9..ab889455c 100644
--- a/capi/geos_c.cpp
+++ b/capi/geos_c.cpp
@@ -1603,6 +1603,18 @@ extern "C" {
         return GEOSPreparedWithin_r(handle, pg1, g2);
     }
 
+    char *
+    GEOSPreparedRelate(const geos::geom::prep::PreparedGeometry* pg1, const Geometry* g2)
+    {
+        return GEOSPreparedRelate_r(handle, pg1, g2);
+    }
+
+    char
+    GEOSPreparedRelatePattern(const geos::geom::prep::PreparedGeometry* pg1, const Geometry* g2, const char* pat)
+    {
+        return GEOSPreparedRelatePattern_r(handle, pg1, g2, pat);
+    }
+
     CoordinateSequence*
     GEOSPreparedNearestPoints(const geos::geom::prep::PreparedGeometry* g1, const Geometry* g2)
     {
diff --git a/capi/geos_c.h.in b/capi/geos_c.h.in
index 124d26524..77ada433a 100644
--- a/capi/geos_c.h.in
+++ b/capi/geos_c.h.in
@@ -1281,6 +1281,19 @@ extern char GEOS_DLL GEOSPreparedWithin_r(
     const GEOSPreparedGeometry* pg1,
     const GEOSGeometry* g2);
 
+/** \see GEOSPreparedRelate */
+extern char GEOS_DLL * GEOSPreparedRelate_r(
+    GEOSContextHandle_t handle,
+    const GEOSPreparedGeometry* pg1,
+    const GEOSGeometry* g2);
+
+/** \see GEOSPreparedRelatePattern */
+extern char GEOS_DLL GEOSPreparedRelatePattern_r(
+    GEOSContextHandle_t handle,
+    const GEOSPreparedGeometry* pg1,
+    const GEOSGeometry* g2,
+    const char* im);
+
 /** \see GEOSPreparedNearestPoints */
 extern GEOSCoordSequence GEOS_DLL *GEOSPreparedNearestPoints_r(
     GEOSContextHandle_t handle,
@@ -5188,6 +5201,44 @@ extern char GEOS_DLL GEOSPreparedWithin(
     const GEOSPreparedGeometry* pg1,
     const GEOSGeometry* g2);
 
+/**
+* Use a \ref GEOSPreparedGeometry do a high performance
+* calculation of the DE9IM relationship between the
+* prepared and provided geometry, and compare that
+* relationship to the provided DE9IM, returning
+* true if the patterns are consistent and false otherwise.
+* \param pg1 The prepared geometry
+* \param g2 The geometry to test
+* \returns The DE9IM relate pattern string
+* \see GEOSPrepare
+* \see GEOSRelate
+* \see GEOSPreparedRelatePattern
+*
+* \since 3.13
+*/
+extern char GEOS_DLL * GEOSPreparedRelate(
+    const GEOSPreparedGeometry* pg1,
+    const GEOSGeometry* g2);
+
+/**
+* Use a \ref GEOSPreparedGeometry do a high performance
+* calculation of the DE9IM relationship between the
+* prepared and provided geometry.
+* \param pg1 The prepared geometry
+* \param g2 The geometry to test
+* \param pat The DE9IM pattern to test
+* \returns 1 on true, 0 on false, 2 on exception
+* \see GEOSPrepare
+* \see GEOSRelatePattern
+* \see GEOSPreparedRelate
+*
+* \since 3.13
+*/
+extern char GEOS_DLL GEOSPreparedRelatePattern(
+    const GEOSPreparedGeometry* pg1,
+    const GEOSGeometry* g2,
+    const char* pat);
+
 /**
 * Use a \ref GEOSPreparedGeometry do a high performance
 * calculation to find the nearest points between the
diff --git a/capi/geos_ts_c.cpp b/capi/geos_ts_c.cpp
index 4581860b6..37b00020e 100644
--- a/capi/geos_ts_c.cpp
+++ b/capi/geos_ts_c.cpp
@@ -92,6 +92,10 @@
 #include <geos/operation/valid/IsValidOp.h>
 #include <geos/operation/valid/MakeValid.h>
 #include <geos/operation/valid/RepeatedPointRemover.h>
+
+#include <geos/operation/relateng/RelateNG.h>
+#include <geos/operation/relateng/RelatePredicate.h>
+
 #include <geos/precision/GeometryPrecisionReducer.h>
 #include <geos/shape/fractal/HilbertEncoder.h>
 #include <geos/simplify/DouglasPeuckerSimplifier.h>
@@ -205,6 +209,8 @@ using geos::operation::geounion::CascadedPolygonUnion;
 using geos::operation::overlayng::OverlayNG;
 using geos::operation::overlayng::UnaryUnionNG;
 using geos::operation::overlayng::OverlayNGRobust;
+using geos::operation::relateng::RelateNG;
+using geos::operation::relateng::RelatePredicate;
 using geos::operation::valid::TopologyValidationError;
 
 using geos::precision::GeometryPrecisionReducer;
@@ -577,7 +583,7 @@ extern "C" {
     GEOSDisjoint_r(GEOSContextHandle_t extHandle, const Geometry* g1, const Geometry* g2)
     {
         return execute(extHandle, 2, [&]() {
-            return g1->disjoint(g2);
+            return RelateNG::disjoint(g1, g2);
         });
     }
 
@@ -585,7 +591,7 @@ extern "C" {
     GEOSTouches_r(GEOSContextHandle_t extHandle, const Geometry* g1, const Geometry* g2)
     {
         return execute(extHandle, 2, [&]() {
-            return g1->touches(g2);
+            return RelateNG::touches(g1, g2);
         });
     }
 
@@ -593,7 +599,7 @@ extern "C" {
     GEOSIntersects_r(GEOSContextHandle_t extHandle, const Geometry* g1, const Geometry* g2)
     {
         return execute(extHandle, 2, [&]() {
-            return g1->intersects(g2);
+            return RelateNG::intersects(g1, g2);
         });
     }
 
@@ -601,7 +607,7 @@ extern "C" {
     GEOSCrosses_r(GEOSContextHandle_t extHandle, const Geometry* g1, const Geometry* g2)
     {
         return execute(extHandle, 2, [&]() {
-            return g1->crosses(g2);
+            return RelateNG::crosses(g1, g2);
         });
     }
 
@@ -609,7 +615,7 @@ extern "C" {
     GEOSWithin_r(GEOSContextHandle_t extHandle, const Geometry* g1, const Geometry* g2)
     {
         return execute(extHandle, 2, [&]() {
-            return g1->within(g2);
+            return RelateNG::within(g1, g2);
         });
     }
 
@@ -617,7 +623,7 @@ extern "C" {
     GEOSContains_r(GEOSContextHandle_t extHandle, const Geometry* g1, const Geometry* g2)
     {
         return execute(extHandle, 2, [&]() {
-            return g1->contains(g2);
+            return RelateNG::contains(g1, g2);
         });
     }
 
@@ -625,7 +631,7 @@ extern "C" {
     GEOSOverlaps_r(GEOSContextHandle_t extHandle, const Geometry* g1, const Geometry* g2)
     {
         return execute(extHandle, 2, [&]() {
-            return g1->overlaps(g2);
+            return RelateNG::overlaps(g1, g2);
         });
     }
 
@@ -633,7 +639,7 @@ extern "C" {
     GEOSCovers_r(GEOSContextHandle_t extHandle, const Geometry* g1, const Geometry* g2)
     {
         return execute(extHandle, 2, [&]() {
-            return g1->covers(g2);
+            return RelateNG::covers(g1, g2);
         });
     }
 
@@ -641,7 +647,15 @@ extern "C" {
     GEOSCoveredBy_r(GEOSContextHandle_t extHandle, const Geometry* g1, const Geometry* g2)
     {
         return execute(extHandle, 2, [&]() {
-            return g1->coveredBy(g2);
+            return RelateNG::coveredBy(g1, g2);
+        });
+    }
+
+    char
+    GEOSEquals_r(GEOSContextHandle_t extHandle, const Geometry* g1, const Geometry* g2)
+    {
+        return execute(extHandle, 2, [&]() {
+            return RelateNG::equalsTopo(g1, g2);
         });
     }
 
@@ -701,19 +715,19 @@ extern "C" {
 
             switch (bnr) {
                 case GEOSRELATE_BNR_MOD2: /* same as OGC */
-                    im = RelateOp::relate(g1, g2,
+                    im = RelateNG::relate(g1, g2,
                                           BoundaryNodeRule::getBoundaryRuleMod2());
                     break;
                 case GEOSRELATE_BNR_ENDPOINT:
-                    im = RelateOp::relate(g1, g2,
+                    im = RelateNG::relate(g1, g2,
                                           BoundaryNodeRule::getBoundaryEndPoint());
                     break;
                 case GEOSRELATE_BNR_MULTIVALENT_ENDPOINT:
-                    im = RelateOp::relate(g1, g2,
+                    im = RelateNG::relate(g1, g2,
                                           BoundaryNodeRule::getBoundaryMultivalentEndPoint());
                     break;
                 case GEOSRELATE_BNR_MONOVALENT_ENDPOINT:
-                    im = RelateOp::relate(g1, g2,
+                    im = RelateNG::relate(g1, g2,
                                           BoundaryNodeRule::getBoundaryMonovalentEndPoint());
                     break;
                 default:
@@ -827,14 +841,6 @@ extern "C" {
 // general purpose
 //-----------------------------------------------------------------
 
-    char
-    GEOSEquals_r(GEOSContextHandle_t extHandle, const Geometry* g1, const Geometry* g2)
-    {
-        return execute(extHandle, 2, [&]() {
-            return g1->equals(g2);
-        });
-    }
-
     char
     GEOSEqualsExact_r(GEOSContextHandle_t extHandle, const Geometry* g1, const Geometry* g2, double tolerance)
     {
@@ -3744,6 +3750,24 @@ extern "C" {
         });
     }
 
+    char *
+    GEOSPreparedRelate_r(GEOSContextHandle_t extHandle,
+                         const geos::geom::prep::PreparedGeometry* pg, const Geometry* g)
+    {
+        return execute(extHandle, [&]() -> char * {
+            return gstrdup(pg->relate(g)->toString());
+        });
+    }
+
+    char
+    GEOSPreparedRelatePattern_r(GEOSContextHandle_t extHandle,
+                         const geos::geom::prep::PreparedGeometry* pg, const Geometry* g, const char* pat)
+    {
+        return execute(extHandle, 2, [&]() {
+            return pg->relate(g, std::string(pat));
+        });
+    }
+
     CoordinateSequence*
     GEOSPreparedNearestPoints_r(GEOSContextHandle_t extHandle,
                          const geos::geom::prep::PreparedGeometry* pg, const Geometry* g)
diff --git a/include/geos/algorithm/PolygonNodeTopology.h b/include/geos/algorithm/PolygonNodeTopology.h
index a1987e3f5..ca3de04f2 100644
--- a/include/geos/algorithm/PolygonNodeTopology.h
+++ b/include/geos/algorithm/PolygonNodeTopology.h
@@ -76,6 +76,21 @@ public:
     static bool isInteriorSegment(const CoordinateXY* nodePt,
         const CoordinateXY* a0, const CoordinateXY* a1, const CoordinateXY* b);
 
+    /**
+    * Compares the angles of two vectors
+    * relative to the positive X-axis at their origin.
+    * Angles increase CCW from the X-axis.
+    *
+    * @param origin the origin of the vectors
+    * @param p the endpoint of the vector P
+    * @param q the endpoint of the vector Q
+    * @return a negative integer, zero, or a positive integer as this vector P has angle less than, equal to, or greater than vector Q
+    */
+    static int compareAngle(
+        const CoordinateXY* origin,
+        const CoordinateXY* p,
+        const CoordinateXY* q);
+
 
 private:
 
@@ -95,6 +110,23 @@ private:
         const CoordinateXY* p,
         const CoordinateXY* e0, const CoordinateXY* e1);
 
+    /**
+    * Compares whether an edge p is between or outside the edges e0 and e1,
+    * where the edges all originate at a common origin.
+    * The "inside" of e0 and e1 is the arc which does not include
+    * the positive X-axis at the origin.
+    * If p is collinear with an edge 0 is returned.
+    *
+    * @param origin the origin
+    * @param p the destination point of edge p
+    * @param e0 the destination point of edge e0
+    * @param e1 the destination point of edge e1
+    * @return a negative integer, zero or positive integer as the vector P lies outside, collinear with, or inside the vectors E0 and E1
+    */
+    static int compareBetween(const CoordinateXY* origin, const CoordinateXY* p,
+        const CoordinateXY* e0, const CoordinateXY* e1);
+
+
     /**
     * Tests if the angle with the origin of a vector P is greater than that of the
     * vector Q.
diff --git a/include/geos/constants.h b/include/geos/constants.h
index 3781bccbf..7be692144 100644
--- a/include/geos/constants.h
+++ b/include/geos/constants.h
@@ -43,6 +43,7 @@ constexpr double DoubleNegInfinity = (-(std::numeric_limits<double>::infinity)()
 constexpr double DoubleEpsilon = std::numeric_limits<double>::epsilon();
 
 constexpr std::size_t NO_COORD_INDEX = std::numeric_limits<std::size_t>::max();
+constexpr std::size_t INDEX_UNKNOWN = std::numeric_limits<std::size_t>::max();
 
 } // namespace geos
 
diff --git a/include/geos/geom/Coordinate.h b/include/geos/geom/Coordinate.h
index 50763083c..0d8bced0f 100644
--- a/include/geos/geom/Coordinate.h
+++ b/include/geos/geom/Coordinate.h
@@ -22,6 +22,7 @@
 #include <vector> // for typedefs
 #include <string>
 #include <limits>
+#include <map>
 
 #ifdef _MSC_VER
 #pragma warning(push)
@@ -222,6 +223,7 @@ private:
 public:
     /// A set of const Coordinate pointers
     typedef std::set<const Coordinate*, CoordinateLessThan> ConstSet;
+    typedef std::set<const CoordinateXY*, CoordinateLessThan> ConstXYSet;
 
     /// A vector of const Coordinate pointers
     typedef std::vector<const Coordinate*> ConstVect;
@@ -232,6 +234,9 @@ public:
     /// A vector of Coordinate objects (real object, not pointers)
     typedef std::vector<Coordinate> Vect;
 
+    /// A map of const Coordinate pointers to integers
+    typedef std::map<const CoordinateXY*, int, CoordinateLessThan> ConstIntMap;
+
     /// z-coordinate
     double z;
 
diff --git a/include/geos/geom/Geometry.h b/include/geos/geom/Geometry.h
index 114f4c039..c2378f4c1 100644
--- a/include/geos/geom/Geometry.h
+++ b/include/geos/geom/Geometry.h
@@ -612,11 +612,7 @@ public:
      * @see Geometry#within
      * @see Geometry#covers
      */
-    bool
-    coveredBy(const Geometry* g) const
-    {
-        return g->covers(this);
-    }
+    bool coveredBy(const Geometry* g) const;
 
 
     /// Returns the Well-known Text representation of this Geometry.
diff --git a/include/geos/geom/GeometryCollection.h b/include/geos/geom/GeometryCollection.h
index ae9a441aa..9437f6af3 100644
--- a/include/geos/geom/GeometryCollection.h
+++ b/include/geos/geom/GeometryCollection.h
@@ -248,6 +248,7 @@ protected:
 
     bool hasCurvedComponents() const override;
 
+
 };
 
 } // namespace geos::geom
diff --git a/include/geos/geom/IntersectionMatrix.h b/include/geos/geom/IntersectionMatrix.h
index ed2ff77c6..c7ad506c5 100644
--- a/include/geos/geom/IntersectionMatrix.h
+++ b/include/geos/geom/IntersectionMatrix.h
@@ -226,7 +226,7 @@ public:
      *
      * @return the dimension value at the given matrix position.
      */
-    int get(geom::Location row, geom::Location column) const {
+    int get(Location row, Location column) const {
         return matrix[static_cast<size_t>(row)][static_cast<size_t>(column)];
     }
 
@@ -363,6 +363,7 @@ public:
      */
     std::string toString() const;
 
+
 private:
 
     static const int firstDim; // = 3;
diff --git a/include/geos/geom/prep/BasicPreparedGeometry.h b/include/geos/geom/prep/BasicPreparedGeometry.h
index 62f8621fa..068ade2bc 100644
--- a/include/geos/geom/prep/BasicPreparedGeometry.h
+++ b/include/geos/geom/prep/BasicPreparedGeometry.h
@@ -20,10 +20,8 @@
 #pragma once
 
 #include <geos/geom/prep/PreparedGeometry.h> // for inheritance
-//#include <geos/algorithm/PointLocator.h>
-//#include <geos/geom/util/ComponentCoordinateExtracter.h>
 #include <geos/geom/Coordinate.h>
-//#include <geos/geom/Location.h>
+#include <geos/operation/relateng/RelateNG.h>
 
 #include <vector>
 #include <string>
@@ -40,6 +38,8 @@ namespace geos {
 namespace geom { // geos::geom
 namespace prep { // geos::geom::prep
 
+using geos::operation::relateng::RelateNG;
+
 // * \class BasicPreparedGeometry
 
 /**
@@ -59,6 +59,15 @@ class BasicPreparedGeometry: public PreparedGeometry {
 private:
     const geom::Geometry* baseGeom;
     std::vector<const CoordinateXY*> representativePts;
+    mutable std::unique_ptr<RelateNG> relate_ng;
+
+    std::unique_ptr<RelateNG>& getRelateNG() const
+    {
+        if (relate_ng == nullptr)
+            relate_ng = RelateNG::prepare(baseGeom);
+
+        return relate_ng;
+    }
 
 protected:
     /**
@@ -119,6 +128,11 @@ public:
      */
     bool isAnyTargetComponentInTest(const geom::Geometry* testGeom) const;
 
+    /**
+     * Default implementation.
+     */
+    bool within(const geom::Geometry* g) const override;
+
     /**
      * Default implementation.
      */
@@ -168,7 +182,12 @@ public:
     /**
      * Default implementation.
      */
-    bool within(const geom::Geometry* g) const override;
+    std::unique_ptr<IntersectionMatrix> relate(const geom::Geometry* g) const override;
+
+    /**
+     * Default implementation.
+     */
+    bool relate(const geom::Geometry* g, const std::string& pat) const override;
 
     /**
      * Default implementation.
diff --git a/include/geos/geom/prep/PreparedGeometry.h b/include/geos/geom/prep/PreparedGeometry.h
index d0cf2379b..6c7751390 100644
--- a/include/geos/geom/prep/PreparedGeometry.h
+++ b/include/geos/geom/prep/PreparedGeometry.h
@@ -20,6 +20,7 @@
 
 #include <vector>
 #include <memory>
+#include <string>
 #include <geos/export.h>
 
 // Forward declarations
@@ -28,6 +29,7 @@ namespace geos {
         class Geometry;
         class Coordinate;
         class CoordinateSequence;
+        class IntersectionMatrix;
     }
 }
 
@@ -227,6 +229,28 @@ public:
      *
      */
     virtual bool isWithinDistance(const geom::Geometry* geom, double dist) const = 0;
+
+    /** \brief
+     * Compares the prepared geometry to the given geometry
+     * and returns the DE9IM intersection matrix as a string.
+     *
+     * @param geom the Geometry to test the
+     * @return the DE9IM matrix
+     */
+    virtual std::unique_ptr<IntersectionMatrix> relate(const geom::Geometry* geom) const = 0;
+
+    /** \brief
+     * Compares the prepared geometry to the given geometry
+     * and the provided DE9IM pattern, and returns true if the
+     * pattern is consistent with the relationship between the
+     * prepared and provided geometries.
+     *
+     * @param geom the Geometry to test the distance to
+     * @param pat the DE9IM pattern
+     * @return true if the patterns are consistent
+     */
+    virtual bool relate(const geom::Geometry* geom, const std::string& pat) const = 0;
+
 };
 
 
diff --git a/include/geos/geom/util/GeometryLister.h b/include/geos/geom/util/GeometryLister.h
new file mode 100644
index 000000000..912518ef1
--- /dev/null
+++ b/include/geos/geom/util/GeometryLister.h
@@ -0,0 +1,92 @@
+/**********************************************************************
+ *
+ * GEOS - Geometry Engine Open Source
+ * http://geos.osgeo.org
+ *
+ * Copyright (C) 2011 Sandro Santilli <strk at kbt.io>
+ * Copyright (C) 2001-2002 Vivid Solutions Inc.
+ * Copyright (C) 2006 Refractions Research Inc.
+ *
+ * This is free software; you can redistribute and/or modify it under
+ * the terms of the GNU Lesser General Public Licence as published
+ * by the Free Software Foundation.
+ * See the COPYING file for more information.
+ *
+ **********************************************************************
+ *
+ * Last port: geom/util/GeometryExtracter.java r320 (JTS-1.12)
+ *
+ **********************************************************************/
+
+#pragma once
+
+#include <geos/export.h>
+#include <geos/geom/GeometryFilter.h>
+#include <geos/geom/GeometryCollection.h>
+#include <vector>
+
+namespace geos {
+namespace geom { // geos.geom
+namespace util { // geos.geom.util
+
+/**
+ * Extracts all the components of a collection, or just echoes back a
+ * pointers to singletons.
+ */
+class GEOS_DLL GeometryLister {
+
+public:
+
+    /**
+     * Extracts the components from a {@link Geometry}
+     * and adds them to the provided container.
+     *
+     * Useful for iterating over components of a collection.
+     *
+     * @param geom the geometry from which to extract
+     * @param lst the list to add the extracted elements to
+     */
+    static void
+    list(const Geometry* geom, std::vector<const Geometry*>& lst)
+    {
+        if(geom->isCollection()) {
+            GeometryLister::Lister lister(lst);
+            geom->apply_ro(&lister);
+        }
+        else {
+            lst.push_back(geom);
+        }
+    }
+
+private:
+
+    struct Lister : public GeometryFilter {
+
+        /**
+         * Constructs a filter with a list in which to store the elements found.
+         *
+         * @param comps the container to extract into (will push_back to it)
+         */
+        Lister(std::vector<const Geometry*>& p_geoms) : geoms(p_geoms) {}
+
+        std::vector<const Geometry*>& geoms;
+
+        void
+        filter_ro(const Geometry* geom) override
+        {
+            if(!geom->isCollection()) {
+                geoms.push_back(geom);
+            }
+        }
+
+        // // Declare type as noncopyable
+        // Lister(const Lister& other);
+        // Lister& operator=(const Lister& rhs);
+    };
+};
+
+
+} // namespace geos.geom.util
+} // namespace geos.geom
+} // namespace geos
+
diff --git a/include/geos/index/chain/MonotoneChain.h b/include/geos/index/chain/MonotoneChain.h
index 4b0dbb427..d6b1b1ad6 100644
--- a/include/geos/index/chain/MonotoneChain.h
+++ b/include/geos/index/chain/MonotoneChain.h
@@ -151,6 +151,9 @@ public:
         return context;
     }
 
+    void setId(int p_id) { id = p_id; }
+    int getId() const { return id; }
+
 private:
 
     void computeSelect(const geom::Envelope& searchEnv,
@@ -197,6 +200,10 @@ private:
 
     /// Owned by this class
     mutable geom::Envelope env;
+
+    /// Useful for optimizing chain comparisons
+    int id = 0;
+
 };
 
 } // namespace geos::index::chain
diff --git a/include/geos/io/WKTWriter.h b/include/geos/io/WKTWriter.h
index 97007dc17..105b63739 100644
--- a/include/geos/io/WKTWriter.h
+++ b/include/geos/io/WKTWriter.h
@@ -121,7 +121,7 @@ public:
      *
      * @return the WKT
      */
-    static std::string toLineString(const geom::Coordinate& p0, const geom::Coordinate& p1);
+    static std::string toLineString(const geom::CoordinateXY& p0, const geom::CoordinateXY& p1);
 
     /**
      * Generates the WKT for a <code>Point</code>.
diff --git a/include/geos/noding/MCIndexSegmentSetMutualIntersector.h b/include/geos/noding/MCIndexSegmentSetMutualIntersector.h
index 12fe5d8b3..770784bb8 100644
--- a/include/geos/noding/MCIndexSegmentSetMutualIntersector.h
+++ b/include/geos/noding/MCIndexSegmentSetMutualIntersector.h
@@ -44,7 +44,7 @@ namespace noding { // geos::noding
  *
  * @version 1.7
  */
-class MCIndexSegmentSetMutualIntersector : public SegmentSetMutualIntersector {
+class GEOS_DLL MCIndexSegmentSetMutualIntersector : public SegmentSetMutualIntersector {
 public:
 
     MCIndexSegmentSetMutualIntersector(double p_tolerance)
diff --git a/include/geos/noding/SegmentString.h b/include/geos/noding/SegmentString.h
index 3d71f009a..af7cee2de 100644
--- a/include/geos/noding/SegmentString.h
+++ b/include/geos/noding/SegmentString.h
@@ -131,6 +131,42 @@ public:
         return ss.getSegmentOctant(index);
     }
 
+    /**
+     * Gets the next vertex in a ring from a vertex index.
+     *
+     * @param index the vertex index
+     * @return the next vertex in the ring
+     *
+     * @see isClosed
+     */
+    const geom::CoordinateXY& nextInRing(std::size_t index) const
+    {
+        std::size_t nextIndex = index + 1;
+        if (nextIndex > size() - 1) {
+            nextIndex = 1;
+        }
+        return getCoordinate(nextIndex);
+    }
+
+    /**
+     * Gets the previous vertex in a ring from a vertex index.
+     *
+     * @param index the vertex index
+     * @return the previous vertex in the ring
+     *
+     * @see isClosed
+     */
+    const geom::CoordinateXY& prevInRing(std::size_t index) const
+    {
+        std::size_t prevIndex;
+        if (index == 0)
+            prevIndex = size() - 2;
+        else
+            prevIndex = index - 1;
+        return getCoordinate( prevIndex );
+    }
+
+
     bool isClosed() const {
         return seq->front<geom::CoordinateXY>().equals(seq->back<geom::CoordinateXY>());
     }
diff --git a/include/geos/operation/relateng/AdjacentEdgeLocator.h b/include/geos/operation/relateng/AdjacentEdgeLocator.h
new file mode 100644
index 000000000..d6e576985
--- /dev/null
+++ b/include/geos/operation/relateng/AdjacentEdgeLocator.h
@@ -0,0 +1,124 @@
+/**********************************************************************
+ *
+ * GEOS - Geometry Engine Open Source
+ * http://geos.osgeo.org
+ *
+ * Copyright (c) 2024 Martin Davis
+ * Copyright (C) 2024 Paul Ramsey <pramsey at cleverelephant.ca>
+ *
+ * This is free software; you can redistribute and/or modify it under
+ * the terms of the GNU Lesser General Public Licence as published
+ * by the Free Software Foundation.
+ * See the COPYING file for more information.
+ *
+ **********************************************************************/
+
+#pragma once
+
+#include <geos/geom/CoordinateSequence.h>
+#include <geos/geom/Location.h>
+#include <geos/export.h>
+
+
+// Forward declarations
+namespace geos {
+namespace operation {
+namespace relateng {
+    class NodeSections;
+    class NodeSection;
+}
+}
+namespace geom {
+    class CoordinateXY;
+    class Geometry;
+    class LinearRing;
+    class Polygon;
+}
+}
+
+
+using geos::geom::CoordinateXY;
+using geos::geom::CoordinateSequence;
+using geos::geom::Geometry;
+using geos::geom::LinearRing;
+using geos::geom::Polygon;
+using geos::geom::Location;
+
+
+
+namespace geos {      // geos.
+namespace operation { // geos.operation
+namespace relateng {  // geos.operation.relateng
+
+/**
+ * Determines the location for a point which is known to lie
+ * on at least one edge of a set of polygons.
+ * This provides the union-semantics for determining
+ * point location in a GeometryCollection, which may
+ * have polygons with adjacent edges which are effectively
+ * in the interior of the geometry.
+ * Note that it is also possible to have adjacent edges which
+ * lie on the boundary of the geometry
+ * (e.g. a polygon contained within another polygon with adjacent edges).
+ *
+ * @author mdavis
+ *
+ */
+class GEOS_DLL AdjacentEdgeLocator {
+
+public:
+
+    AdjacentEdgeLocator(const Geometry* geom)
+    {
+        init(geom);
+    }
+
+    Location locate(const CoordinateXY* p);
+
+    /**
+     * Disable copy construction and assignment. Apparently needed to make this
+     * class compile under MSVC. (See https://stackoverflow.com/q/29565299)
+     */
+    AdjacentEdgeLocator(const AdjacentEdgeLocator&) = delete;
+    AdjacentEdgeLocator& operator=(const AdjacentEdgeLocator&) = delete;
+
+
+private:
+
+    // Members
+
+    std::vector<const CoordinateSequence*> ringList;
+
+    /*
+     * When we have to reorient rings, we end up allocating new
+     * rings, since we cannot reorient the rings of the input
+     * geometry, so this is where we store those "local" rings.
+     */
+    std::vector<std::unique_ptr<CoordinateSequence>> localRingList;
+
+
+    // Methods
+
+    void addSections(
+        const CoordinateXY* p,
+        const CoordinateSequence* ring,
+        NodeSections& sections);
+
+    NodeSection* createSection(
+        const CoordinateXY* p,
+        const CoordinateXY* prev,
+        const CoordinateXY* next);
+
+    void init(const Geometry* geom);
+
+    void addRings(const Geometry* geom);
+
+    void addRing(const LinearRing* ring, bool requireCW);
+
+
+};
+
+} // namespace geos.operation.relateng
+} // namespace geos.operation
+} // namespace geos
+
diff --git a/include/geos/operation/relateng/BasicPredicate.h b/include/geos/operation/relateng/BasicPredicate.h
new file mode 100644
index 000000000..2ed3f0539
--- /dev/null
+++ b/include/geos/operation/relateng/BasicPredicate.h
@@ -0,0 +1,105 @@
+/**********************************************************************
+ *
+ * GEOS - Geometry Engine Open Source
+ * http://geos.osgeo.org
+ *
+ * Copyright (c) 2024 Martin Davis
+ * Copyright (C) 2024 Paul Ramsey <pramsey at cleverelephant.ca>
+ *
+ * This is free software; you can redistribute and/or modify it under
+ * the terms of the GNU Lesser General Public Licence as published
+ * by the Free Software Foundation.
+ * See the COPYING file for more information.
+ *
+ **********************************************************************/
+
+#pragma once
+
+#include <geos/geom/Location.h>
+#include <geos/operation/relateng/TopologyPredicate.h>
+#include <geos/export.h>
+
+#include <memory>
+#include <vector>
+
+// Forward declarations
+namespace geos {
+namespace geom {
+    class Envelope;
+}
+}
+
+
+using geos::geom::Envelope;
+using geos::geom::Location;
+
+
+namespace geos {      // geos.
+namespace operation { // geos.operation.
+namespace relateng {  // geos.operation.relateng
+
+
+class GEOS_DLL BasicPredicate : public TopologyPredicate {
+
+private:
+
+    static constexpr int UNKNOWN = -1;
+    static constexpr int FALSE = 0;
+    static constexpr int TRUE = 1;
+
+    int m_value = UNKNOWN;
+
+    static bool isKnown(int val);
+
+    static bool toBoolean(int val);
+
+    static int toValue(bool val);
+
+
+protected:
+
+    /**
+    * Updates the predicate value to the given state
+    * if it is currently unknown.
+    *
+    * @param val the predicate value to update
+    */
+    void setValue(bool val);
+
+    void setValue(int val);
+
+    void setValueIf(bool val, bool cond);
+
+    void require(bool cond);
+
+    using TopologyPredicate::requireCovers;
+    void requireCovers(const Envelope& a, const Envelope& b);
+
+
+public:
+
+    /**
+    * Tests if two geometries intersect
+    * based on an interaction at given locations.
+    *
+    * @param locA the location on geometry A
+    * @param locB the location on geometry B
+    * @return true if the geometries intersect
+    */
+    static bool isIntersection(Location locA, Location locB);
+
+    std::string name() const override = 0;
+
+    void finish() override = 0;
+
+    bool isKnown() const override;
+
+    bool value() const override;
+
+
+};
+
+} // namespace geos.operation.relateng
+} // namespace geos.operation
+} // namespace geos
+
diff --git a/include/geos/operation/relateng/DimensionLocation.h b/include/geos/operation/relateng/DimensionLocation.h
new file mode 100644
index 000000000..6d3d87050
--- /dev/null
+++ b/include/geos/operation/relateng/DimensionLocation.h
@@ -0,0 +1,60 @@
+/**********************************************************************
+ *
+ * GEOS - Geometry Engine Open Source
+ * http://geos.osgeo.org
+ *
+ * Copyright (c) 2024 Martin Davis
+ * Copyright (C) 2024 Paul Ramsey <pramsey at cleverelephant.ca>
+ *
+ * This is free software; you can redistribute and/or modify it under
+ * the terms of the GNU Lesser General Public Licence as published
+ * by the Free Software Foundation.
+ * See the COPYING file for more information.
+ *
+ **********************************************************************/
+
+#pragma once
+
+#include <geos/geom/Location.h>
+#include <geos/export.h>
+
+
+using geos::geom::Location;
+
+
+namespace geos {      // geos.
+namespace operation { // geos.operation
+namespace relateng { // geos.operation.relateng
+
+
+class GEOS_DLL DimensionLocation {
+
+public:
+
+    enum DimensionLocationType {
+        EXTERIOR = 2,           // == Location.EXTERIOR
+        POINT_INTERIOR = 103,
+        LINE_INTERIOR = 110,
+        LINE_BOUNDARY = 111,
+        AREA_INTERIOR = 120,
+        AREA_BOUNDARY = 121
+    };
+
+    static int locationArea(Location loc);
+
+    static int locationLine(Location loc);
+
+    static int locationPoint(Location loc);
+
+    static Location location(int dimLoc);
+
+    static int dimension(int dimLoc);
+
+    static int dimension(int dimLoc, int exteriorDim);
+
+};
+
+} // namespace geos.operation.relateng
+} // namespace geos.operation
+} // namespace geos
+
diff --git a/include/geos/operation/relateng/EdgeSegmentIntersector.h b/include/geos/operation/relateng/EdgeSegmentIntersector.h
new file mode 100644
index 000000000..1b05b6f13
--- /dev/null
+++ b/include/geos/operation/relateng/EdgeSegmentIntersector.h
@@ -0,0 +1,80 @@
+/**********************************************************************
+ *
+ * GEOS - Geometry Engine Open Source
+ * http://geos.osgeo.org
+ *
+ * Copyright (c) 2024 Martin Davis
+ * Copyright (C) 2024 Paul Ramsey <pramsey at cleverelephant.ca>
+ *
+ * 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/noding/SegmentIntersector.h>
+#include <geos/algorithm/LineIntersector.h>
+#include <geos/export.h>
+
+
+// Forward declarations
+namespace geos {
+namespace noding {
+    class SegmentString;
+}
+namespace operation {
+namespace relateng {
+    class RelateSegmentString;
+    class TopologyComputer;
+}
+}
+}
+
+
+using geos::noding::SegmentIntersector;
+using geos::noding::SegmentString;
+using geos::algorithm::LineIntersector;
+
+
+namespace geos {      // geos.
+namespace operation { // geos.operation
+namespace relateng { // geos.operation.relateng
+
+class GEOS_DLL EdgeSegmentIntersector : public SegmentIntersector {
+
+private:
+
+    // Members
+    LineIntersector li;
+    TopologyComputer& topoComputer;
+
+    // Methods
+
+
+    void addIntersections(
+        RelateSegmentString* ssA, std::size_t segIndexA,
+        RelateSegmentString* ssB, std::size_t segIndexB);
+
+
+public:
+
+    EdgeSegmentIntersector(TopologyComputer& p_topoComputer)
+        : topoComputer(p_topoComputer)
+        {};
+
+    void processIntersections(
+        SegmentString* ss0, std::size_t segIndex0,
+        SegmentString* ss1, std::size_t segIndex1) override;
+
+    bool isDone() const override;
+
+};
+
+} // namespace geos.operation.relateng
+} // namespace geos.operation
+} // namespace geos
+
diff --git a/include/geos/operation/relateng/EdgeSegmentOverlapAction.h b/include/geos/operation/relateng/EdgeSegmentOverlapAction.h
new file mode 100644
index 000000000..7f84ec797
--- /dev/null
+++ b/include/geos/operation/relateng/EdgeSegmentOverlapAction.h
@@ -0,0 +1,75 @@
+/**********************************************************************
+ *
+ * GEOS - Geometry Engine Open Source
+ * http://geos.osgeo.org
+ *
+ * Copyright (C) 2001-2002 Vivid Solutions Inc.
+ *
+ * This is free software; you can redistribute and/or modify it under
+ * the terms of the GNU Lesser General Public Licence as published
+ * by the Free Software Foundation.
+ * See the COPYING file for more information.
+ *
+ **********************************************************************
+ *
+ * Last port: index/chain/MonotoneChainOverlapAction.java rev. 1.6 (JTS-1.10)
+ *
+ **********************************************************************/
+
+#pragma once
+
+#include <geos/export.h>
+#include <geos/geom/LineSegment.h>
+#include <geos/index/chain/MonotoneChainOverlapAction.h>
+
+
+// Forward declarations
+namespace geos {
+namespace index {
+namespace chain {
+    class MonotoneChain;
+}
+}
+namespace noding {
+    class SegmentIntersector;
+}
+}
+
+
+using geos::index::chain::MonotoneChain;
+using geos::index::chain::MonotoneChainOverlapAction;
+using geos::noding::SegmentIntersector;
+
+
+namespace geos {
+namespace operation { // geos::operation
+namespace relateng {  // geos::operation::relateng
+
+/** \brief
+ * The action for the internal iterator for performing
+ * overlap queries on a MonotoneChain.
+ */
+class GEOS_DLL EdgeSegmentOverlapAction : public MonotoneChainOverlapAction {
+
+private:
+
+    SegmentIntersector& si;
+
+
+public:
+
+    EdgeSegmentOverlapAction(SegmentIntersector& p_si)
+        : si(p_si)
+        {}
+
+    void overlap(
+        const MonotoneChain& mc1, std::size_t start1,
+        const MonotoneChain& mc2, std::size_t start2) override;
+
+
+};
+
+} // namespace geos::index::chain
+} // namespace geos::index
+} // namespace geos
+
diff --git a/include/geos/operation/relateng/EdgeSetIntersector.h b/include/geos/operation/relateng/EdgeSetIntersector.h
new file mode 100644
index 000000000..70b6cb315
--- /dev/null
+++ b/include/geos/operation/relateng/EdgeSetIntersector.h
@@ -0,0 +1,95 @@
+/**********************************************************************
+ *
+ * GEOS - Geometry Engine Open Source
+ * http://geos.osgeo.org
+ *
+ * Copyright (c) 2024 Martin Davis
+ * Copyright (C) 2024 Paul Ramsey <pramsey at cleverelephant.ca>
+ *
+ * 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/index/strtree/TemplateSTRtree.h>
+#include <geos/index/chain/MonotoneChain.h>
+#include <geos/export.h>
+
+
+// Forward declarations
+namespace geos {
+namespace geom {
+    class Geometry;
+    class Envelope;
+}
+namespace noding {
+    class SegmentString;
+}
+namespace operation {
+namespace relateng {
+    class RelateSegmentString;
+    class EdgeSegmentIntersector;
+}
+}
+}
+
+
+using geos::geom::Envelope;
+using geos::geom::Geometry;
+using geos::index::strtree::TemplateSTRtree;
+using geos::index::chain::MonotoneChain;
+using geos::operation::relateng::EdgeSegmentIntersector;
+
+
+namespace geos {      // geos.
+namespace operation { // geos.operation
+namespace relateng { // geos.operation.relateng
+
+class GEOS_DLL EdgeSetIntersector {
+
+private:
+
+    // Members
+    TemplateSTRtree<const MonotoneChain*> index;
+    // HPRtree index = new HPRtree();
+    const Envelope* envelope = nullptr;
+    std::deque<MonotoneChain> monoChains;
+    int idCounter = 0;
+
+
+    // Methods
+
+    void addToIndex(const SegmentString* segStr);
+
+    void addEdges(std::vector<const SegmentString*>& segStrings);
+
+
+public:
+
+    EdgeSetIntersector(
+        std::vector<const SegmentString*>& edgesA,
+        std::vector<const SegmentString*>& edgesB,
+        const Envelope* env)
+        : envelope(env)
+        , idCounter(0)
+        {
+            addEdges(edgesA);
+            addEdges(edgesB);
+            // build index to ensure thread-safety
+            // index.build();
+        };
+
+    void process(EdgeSegmentIntersector& intersector);
+
+
+};
+
+} // namespace geos.operation.relateng
+} // namespace geos.operation
+} // namespace geos
+
diff --git a/include/geos/operation/relateng/IMPatternMatcher.h b/include/geos/operation/relateng/IMPatternMatcher.h
new file mode 100644
index 000000000..be8486c57
--- /dev/null
+++ b/include/geos/operation/relateng/IMPatternMatcher.h
@@ -0,0 +1,87 @@
+/**********************************************************************
+ *
+ * GEOS - Geometry Engine Open Source
+ * http://geos.osgeo.org
+ *
+ * Copyright (c) 2024 Martin Davis
+ * Copyright (C) 2024 Paul Ramsey <pramsey at cleverelephant.ca>
+ *
+ * This is free software; you can redistribute and/or modify it under
+ * the terms of the GNU Lesser General Public Licence as published
+ * by the Free Software Foundation.
+ * See the COPYING file for more information.
+ *
+ **********************************************************************/
+
+#pragma once
+
+#include <geos/geom/Location.h>
+#include <geos/geom/Dimension.h>
+#include <geos/geom/IntersectionMatrix.h>
+#include <geos/operation/relateng/IMPredicate.h>
+#include <geos/export.h>
+
+#include <memory>
+#include <vector>
+
+// Forward declarations
+namespace geos {
+namespace geom {
+    class Envelope;
+}
+}
+
+
+using geos::geom::Envelope;
+using geos::geom::Location;
+using geos::geom::Dimension;
+using geos::geom::IntersectionMatrix;
+
+
+namespace geos {      // geos.
+namespace operation { // geos.operation.
+namespace relateng {  // geos.operation.relateng
+
+
+class GEOS_DLL IMPatternMatcher : public IMPredicate {
+
+
+private:
+
+    std::string imPattern;
+    IntersectionMatrix patternMatrix;
+
+    static bool requireInteraction(const IntersectionMatrix& im);
+
+    static bool isInteraction(int imDim);
+
+
+public:
+
+    IMPatternMatcher(std::string p_imPattern)
+        : imPattern(p_imPattern)
+        , patternMatrix(p_imPattern)
+        {};
+
+    std::string name() const override;
+
+    using IMPredicate::init;
+    void init(const Envelope& envA, const Envelope& envB) override;
+
+    bool requireInteraction() const override;
+
+    bool isDetermined() const override;
+
+    bool valueIM() override;
+
+    std::string toString() const;
+
+    friend std::ostream& operator<<(std::ostream& os, const IMPatternMatcher& imp);
+
+};
+
+
+} // namespace geos.operation.relateng
+} // namespace geos.operation
+} // namespace geos
+
diff --git a/include/geos/operation/relateng/IMPredicate.h b/include/geos/operation/relateng/IMPredicate.h
new file mode 100644
index 000000000..6cf9f9b10
--- /dev/null
+++ b/include/geos/operation/relateng/IMPredicate.h
@@ -0,0 +1,131 @@
+/**********************************************************************
+ *
+ * GEOS - Geometry Engine Open Source
+ * http://geos.osgeo.org
+ *
+ * Copyright (c) 2024 Martin Davis
+ * Copyright (C) 2024 Paul Ramsey <pramsey at cleverelephant.ca>
+ *
+ * This is free software; you can redistribute and/or modify it under
+ * the terms of the GNU Lesser General Public Licence as published
+ * by the Free Software Foundation.
+ * See the COPYING file for more information.
+ *
+ **********************************************************************/
+
+#pragma once
+
+#include <geos/geom/Location.h>
+#include <geos/geom/Dimension.h>
+#include <geos/geom/IntersectionMatrix.h>
+#include <geos/operation/relateng/BasicPredicate.h>
+#include <geos/export.h>
+
+#include <memory>
+#include <vector>
+
+// Forward declarations
+namespace geos {
+namespace geom {
+    class Envelope;
+}
+}
+
+
+using geos::geom::Envelope;
+using geos::geom::Location;
+using geos::geom::Dimension;
+using geos::geom::IntersectionMatrix;
+
+
+namespace geos {      // geos.
+namespace operation { // geos.operation.
+namespace relateng {  // geos.operation.relateng
+
+
+class GEOS_DLL IMPredicate : public BasicPredicate {
+
+private:
+
+
+
+protected:
+
+    static constexpr int DIM_UNKNOWN = Dimension::DONTCARE;
+
+    int dimA;
+    int dimB;
+    IntersectionMatrix intMatrix;
+
+    /**
+     * Gets the value of the predicate according to the current
+     * intersection matrix state.
+     *
+     * @return the current predicate value
+     */
+    virtual bool valueIM() = 0;
+
+    /**
+     * Tests whether predicate evaluation can be short-circuited
+     * due to the current state of the matrix providing
+     * enough information to determine the predicate value.
+     *
+     * If this value is true then valueIM()
+     * must provide the correct result of the predicate.
+     *
+     * @return true if the predicate value is determined
+     */
+    virtual bool isDetermined() const = 0;
+
+    /**
+     * Tests whether the exterior of the specified input geometry
+     * is intersected by any part of the other input.
+     *
+     * @param isA the input geometry
+     * @return true if the input geometry exterior is intersected
+     */
+    bool intersectsExteriorOf(bool isA) const;
+
+    bool isIntersects(Location locA, Location locB) const;
+
+
+public:
+
+    IMPredicate()
+    {
+        // intMatrix = new IntersectionMatrix();
+        //-- E/E is always dim = 2
+        intMatrix.set(Location::EXTERIOR, Location::EXTERIOR, Dimension::A);
+    }
+
+    static bool isDimsCompatibleWithCovers(int dim0, int dim1);
+
+    void init(int dA, int dB) override;
+
+    void updateDimension(Location locA, Location locB, int dimension) override;
+
+    bool isDimChanged(Location locA, Location locB, int dimension) const;
+
+    using TopologyPredicate::isKnown;
+    bool isKnown(Location locA, Location locB) const;
+
+    bool isDimension(Location locA, Location locB, int dimension) const;
+
+    int getDimension(Location locA, Location locB) const;
+
+    /**
+     * Sets the final value based on the state of the IM.
+     */
+    void finish() override;
+
+    std::string toString() const;
+
+    friend std::ostream& operator<<(std::ostream& os, const IMPredicate& imp);
+
+
+};
+
+} // namespace geos.operation.relateng
+} // namespace geos.operation
+} // namespace geos
+
diff --git a/include/geos/operation/relateng/IntersectionMatrixPattern.h b/include/geos/operation/relateng/IntersectionMatrixPattern.h
new file mode 100644
index 000000000..9e7f2f504
--- /dev/null
+++ b/include/geos/operation/relateng/IntersectionMatrixPattern.h
@@ -0,0 +1,65 @@
+/**********************************************************************
+ *
+ * GEOS - Geometry Engine Open Source
+ * http://geos.osgeo.org
+ *
+ * Copyright (c) 2024 Martin Davis
+ * Copyright (C) 2024 Paul Ramsey <pramsey at cleverelephant.ca>
+ *
+ * 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 <string>
+
+
+namespace geos {      // geos.
+namespace operation { // geos.operation
+namespace relateng {  // geos.operation.relateng
+
+
+class GEOS_DLL IntersectionMatrixPattern {
+
+private:
+
+    /**
+     * Cannot be instantiated.
+     */
+    IntersectionMatrixPattern() {};
+
+
+public:
+
+    /**
+     * A DE-9IM pattern to detect whether two polygonal geometries are adjacent along
+     * an edge, but do not overlap.
+     */
+    static constexpr const char* ADJACENT = "F***1****";
+
+    /**
+     * A DE-9IM pattern to detect a geometry which properly contains another
+     * geometry (i.e. which lies entirely in the interior of the first geometry).
+     */
+    static constexpr const char* CONTAINS_PROPERLY = "T**FF*FF*";
+
+    /**
+     * A DE-9IM pattern to detect if two geometries intersect in their interiors.
+     * This can be used to determine if a polygonal coverage contains any overlaps
+     * (although not whether they are correctly noded).
+     */
+    static constexpr const char* INTERIOR_INTERSECTS = "T********";
+
+
+};
+
+} // namespace geos.operation.relateng
+} // namespace geos.operation
+} // namespace geos
+
diff --git a/include/geos/operation/relateng/LineStringExtracter.h b/include/geos/operation/relateng/LineStringExtracter.h
new file mode 100644
index 000000000..fc7298f88
--- /dev/null
+++ b/include/geos/operation/relateng/LineStringExtracter.h
@@ -0,0 +1,77 @@
+/**********************************************************************
+ *
+ * GEOS - Geometry Engine Open Source
+ * http://geos.osgeo.org
+ *
+ * Copyright (c) 2024 Martin Davis
+ * Copyright (C) 2024 Paul Ramsey <pramsey at cleverelephant.ca>
+ *
+ * This is free software; you can redistribute and/or modify it under
+ * the terms of the GNU Lesser General Public Licence as published
+ * by the Free Software Foundation.
+ * See the COPYING file for more information.
+ *
+ **********************************************************************/
+
+#pragma once
+
+#include <geos/geom/GeometryFilter.h>
+#include <geos/export.h>
+
+#include <memory>
+#include <vector>
+
+// Forward declarations
+namespace geos {
+namespace geom {
+    class LineString;
+    class Geometry;
+}
+}
+
+
+using geos::geom::LineString;
+using geos::geom::Geometry;
+using geos::geom::GeometryFilter;
+
+
+namespace geos {      // geos.
+namespace operation { // geos.operation
+namespace relateng {  // geos.operation.relateng
+
+
+class GEOS_DLL LineStringExtracter : public GeometryFilter {
+
+private:
+
+    std::vector<const LineString*>& comps;
+
+
+public:
+
+    LineStringExtracter(std::vector<const LineString*>& p_comps)
+        : comps(p_comps)
+        {}
+
+    void filter_ro(const geom::Geometry* geom) override;
+
+    static void getLines(const Geometry* geom, std::vector<const LineString*>& lines);
+
+    static std::vector<const LineString*> getLines(const Geometry* geom);
+
+    /**
+    * Extracts the {@link LineString} elements from a single {@link Geometry}
+    * and returns them as either a {@link LineString} or {@link MultiLineString}.
+    *
+    * @param geom the geometry from which to extract
+    * @return a linear geometry
+    */
+    // static std::unique_ptr<Geometry> getGeometry(const Geometry* geom);
+
+};
+
+
+} // namespace geos.operation.relateng
+} // namespace geos.operation
+} // namespace geos
+
diff --git a/include/geos/operation/relateng/LinearBoundary.h b/include/geos/operation/relateng/LinearBoundary.h
new file mode 100644
index 000000000..18032663b
--- /dev/null
+++ b/include/geos/operation/relateng/LinearBoundary.h
@@ -0,0 +1,88 @@
+/**********************************************************************
+ *
+ * GEOS - Geometry Engine Open Source
+ * http://geos.osgeo.org
+ *
+ * Copyright (c) 2024 Martin Davis
+ * Copyright (C) 2024 Paul Ramsey <pramsey at cleverelephant.ca>
+ *
+ * This is free software; you can redistribute and/or modify it under
+ * the terms of the GNU Lesser General Public Licence as published
+ * by the Free Software Foundation.
+ * See the COPYING file for more information.
+ *
+ **********************************************************************/
+
+#pragma once
+
+#include <geos/geom/Coordinate.h>
+#include <geos/export.h>
+
+#include <memory>
+#include <vector>
+
+// Forward declarations
+namespace geos {
+namespace algorithm {
+    class BoundaryNodeRule;
+}
+namespace geom {
+    class CoordinateXY;
+    class LineString;
+}
+}
+
+
+using geos::algorithm::BoundaryNodeRule;
+using geos::geom::Coordinate;
+using geos::geom::CoordinateXY;
+using geos::geom::LineString;
+
+
+namespace geos {      // geos.
+namespace operation { // geos.operation
+namespace relateng { // geos.operation.relateng
+
+class GEOS_DLL LinearBoundary {
+
+private:
+
+    // Members
+
+    Coordinate::ConstIntMap m_vertexDegree;
+    bool m_hasBoundary;
+    const BoundaryNodeRule& m_boundaryNodeRule;
+
+
+public:
+
+    // Constructors
+
+    LinearBoundary(std::vector<const LineString*>& lines, const BoundaryNodeRule& bnRule);
+
+    bool hasBoundary() const;
+
+    bool isBoundary(const CoordinateXY* pt) const;
+
+
+private:
+
+    // Methods
+
+    bool checkBoundary(Coordinate::ConstIntMap& vertexDegree) const;
+
+    static void computeBoundaryPoints(
+        std::vector<const LineString*>& lines,
+        Coordinate::ConstIntMap& vertexDegree);
+
+    static void addEndpoint(
+        const CoordinateXY *p,
+        Coordinate::ConstIntMap& vertexDegree);
+
+
+};
+
+} // namespace geos.operation.relateng
+} // namespace geos.operation
+} // namespace geos
+
diff --git a/include/geos/operation/relateng/NodeSection.h b/include/geos/operation/relateng/NodeSection.h
new file mode 100644
index 000000000..8fe00870e
--- /dev/null
+++ b/include/geos/operation/relateng/NodeSection.h
@@ -0,0 +1,166 @@
+/**********************************************************************
+ *
+ * GEOS - Geometry Engine Open Source
+ * http://geos.osgeo.org
+ *
+ * Copyright (c) 2024 Martin Davis
+ * Copyright (C) 2024 Paul Ramsey <pramsey at cleverelephant.ca>
+ *
+ * This is free software; you can redistribute and/or modify it under
+ * the terms of the GNU Lesser General Public Licence as published
+ * by the Free Software Foundation.
+ * See the COPYING file for more information.
+ *
+ **********************************************************************/
+
+#pragma once
+
+#include <geos/export.h>
+#include <geos/geom/Coordinate.h>
+
+#include <string>
+#include <sstream>
+
+// Forward declarations
+namespace geos {
+namespace geom {
+    class Geometry;
+}
+}
+
+
+using geos::geom::CoordinateXY;
+using geos::geom::Geometry;
+
+
+namespace geos {      // geos.
+namespace operation { // geos.operation
+namespace relateng { // geos.operation.relateng
+
+/**
+ * Represents a computed node along with the incident edges on either side of
+ * it (if they exist).
+ * This captures the information about a node in a geometry component
+ * required to determine the component's contribution to the node topology.
+ * A node in an area geometry always has edges on both sides of the node.
+ * A node in a linear geometry may have one or other incident edge missing, if
+ * the node occurs at an endpoint of the line.
+ * The edges of an area node are assumed to be provided
+ * with CW-shell orientation (as per JTS norm).
+ * This must be enforced by the caller.
+ *
+ * @author Martin Davis
+ *
+ */
+class GEOS_DLL NodeSection {
+
+private:
+
+    // Members
+    bool m_isA;
+    int m_dim;
+    int m_id;
+    int m_ringId;
+    const Geometry* m_poly;
+    bool m_isNodeAtVertex;
+    const CoordinateXY* m_v0;
+    const CoordinateXY m_nodePt;
+    const CoordinateXY* m_v1;
+
+    // Methods
+
+    static int compareWithNull(const CoordinateXY* v0, const CoordinateXY* v1);
+
+    static int compare(int a, int b);
+
+public:
+
+    NodeSection(
+        bool isA,
+        int dim,
+        int id,
+        int ringId,
+        const Geometry* poly,
+        bool isNodeAtVertex,
+        const CoordinateXY* v0,
+        const CoordinateXY nodePt,
+        const CoordinateXY* v1)
+        : m_isA(isA)
+        , m_dim(dim)
+        , m_id(id)
+        , m_ringId(ringId)
+        , m_poly(poly)
+        , m_isNodeAtVertex(isNodeAtVertex)
+        , m_v0(v0)
+        , m_nodePt(nodePt)
+        , m_v1(v1)
+        {};
+
+    NodeSection(const NodeSection* ns)
+        : m_isA(ns->isA())
+        , m_dim(ns->dimension())
+        , m_id(ns->id())
+        , m_ringId(ns->ringId())
+        , m_poly(ns->getPolygonal())
+        , m_isNodeAtVertex(ns->isNodeAtVertex())
+        , m_v0(ns->getVertex(0))
+        , m_nodePt(ns->nodePt())
+        , m_v1(ns->getVertex(1))
+        {};
+
+    const CoordinateXY* getVertex(int i) const;
+
+    const CoordinateXY& nodePt() const;
+
+    int dimension() const;
+
+    int id() const;
+
+    int ringId() const;
+
+    /**
+    * Gets the polygon this section is part of.
+    * Will be null if section is not on a polygon boundary.
+    *
+    * @return the associated polygon, or null
+    */
+    const Geometry* getPolygonal() const;
+
+    bool isShell() const;
+
+    bool isArea() const;
+
+    static bool isAreaArea(const NodeSection& a, const NodeSection& b);
+
+    bool isA() const;
+
+    bool isSameGeometry(const NodeSection& ns) const;
+
+    bool isSamePolygon(const NodeSection& ns) const;
+
+    bool isNodeAtVertex() const;
+
+    bool isProper() const;
+
+    static bool isProper(const NodeSection& a, const NodeSection& b);
+
+    std::string toString() const;
+
+    static std::string edgeRep(const CoordinateXY* p0, const CoordinateXY* p1);
+
+    friend std::ostream& operator<<(std::ostream& os, const NodeSection& ns);
+
+    /**
+    * Compare node sections by parent geometry, dimension, element id and ring id,
+    * and edge vertices.
+    * Sections are assumed to be at the same node point.
+    */
+    int compareTo(const NodeSection& o) const;
+
+
+};
+
+} // namespace geos.operation.relateng
+} // namespace geos.operation
+} // namespace geos
+
diff --git a/include/geos/operation/relateng/NodeSections.h b/include/geos/operation/relateng/NodeSections.h
new file mode 100644
index 000000000..d7d08b342
--- /dev/null
+++ b/include/geos/operation/relateng/NodeSections.h
@@ -0,0 +1,102 @@
+/**********************************************************************
+ *
+ * GEOS - Geometry Engine Open Source
+ * http://geos.osgeo.org
+ *
+ * Copyright (c) 2024 Martin Davis
+ * Copyright (C) 2024 Paul Ramsey <pramsey at cleverelephant.ca>
+ *
+ * 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 <vector>
+#include <memory>
+#include <geos/operation/relateng/NodeSection.h>
+#include <geos/export.h>
+
+
+// Forward declarations
+namespace geos {
+namespace operation {
+namespace relateng {
+class RelateNode;
+// class NodeSection;
+}
+}
+namespace geom {
+    class CoordinateXY;
+    class Geometry;
+}
+}
+
+
+using geos::geom::CoordinateXY;
+using geos::geom::Geometry;
+
+
+namespace geos {      // geos.
+namespace operation { // geos.operation
+namespace relateng { // geos.operation.relateng
+
+
+class GEOS_DLL NodeSections {
+
+private:
+
+    // Members
+    const CoordinateXY* nodePt;
+    std::vector<std::unique_ptr<NodeSection>> sections;
+
+    // Methods
+
+    /**
+    * Sorts the sections so that:
+    *  * lines are before areas
+    *  * edges from the same polygon are contiguous
+    */
+    void prepareSections();
+
+    static bool hasMultiplePolygonSections(
+        std::vector<std::unique_ptr<NodeSection>>& sections,
+        std::size_t i);
+
+    static std::vector<const NodeSection*> collectPolygonSections(
+        std::vector<std::unique_ptr<NodeSection>>& sections,
+        std::size_t i);
+
+
+public:
+
+    NodeSections(const CoordinateXY* pt)
+        : nodePt(pt)
+        {};
+
+    const CoordinateXY* getCoordinate() const;
+
+    void addNodeSection(NodeSection* e);
+
+    bool hasInteractionAB() const;
+
+    const Geometry* getPolygonal(bool isA) const;
+
+    std::unique_ptr<RelateNode> createNode();
+
+    /**
+     * Disable copy construction and assignment. Apparently needed to make this
+     * class compile under MSVC. (See https://stackoverflow.com/q/29565299)
+     */
+    NodeSections(const NodeSections&) = delete;
+    NodeSections& operator=(const NodeSections&) = delete;
+
+};
+
+} // namespace geos.operation.relateng
+} // namespace geos.operation
+} // namespace geos
+
diff --git a/include/geos/operation/relateng/PolygonNodeConverter.h b/include/geos/operation/relateng/PolygonNodeConverter.h
new file mode 100644
index 000000000..0b1ddce35
--- /dev/null
+++ b/include/geos/operation/relateng/PolygonNodeConverter.h
@@ -0,0 +1,112 @@
+/**********************************************************************
+ *
+ * GEOS - Geometry Engine Open Source
+ * http://geos.osgeo.org
+ *
+ * Copyright (c) 2024 Martin Davis
+ * Copyright (C) 2024 Paul Ramsey <pramsey at cleverelephant.ca>
+ *
+ * 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/operation/relateng/NodeSection.h>
+#include <geos/export.h>
+#include <vector>
+#include <memory>
+
+// Forward declarations
+namespace geos {
+namespace operation {
+namespace relateng {
+// class NodeSection;
+}
+}
+}
+
+
+// using geos::geom::CoordinateXY;
+// using geos::geom::Geometry;
+
+
+namespace geos {      // geos.
+namespace operation { // geos.operation
+namespace relateng {  // geos.operation.relateng
+
+/**
+ * Converts the node sections at a polygon node where
+ * a shell and one or more holes touch, or two or more holes touch.
+ * This converts the node topological structure from
+ * the OGC "touching-rings" (AKA "minimal-ring") model to the equivalent "self-touch"
+ * (AKA "inverted/exverted ring" or "maximal ring") model.
+ * In the "self-touch" model the converted NodeSection corners enclose areas
+ * which all lies inside the polygon
+ * (i.e. they does not enclose hole edges).
+ * This allows RelateNode to use simple area-additive semantics
+ * for adding edges and propagating edge locations.
+ *
+ * The input node sections are assumed to have canonical orientation
+ * (CW shells and CCW holes).
+ * The arrangement of shells and holes must be topologically valid.
+ * Specifically, the node sections must not cross or be collinear.
+ *
+ * This supports multiple shell-shell touches
+ * (including ones containing holes), and hole-hole touches,
+ * This generalizes the relate algorithm to support
+ * both the OGC model and the self-touch model.
+ *
+ * @author Martin Davis
+ * @see RelateNode
+ */
+class GEOS_DLL PolygonNodeConverter {
+
+public:
+
+    /**
+    * Converts a list of sections of valid polygon rings
+    * to have "self-touching" structure.
+    * There are the same number of output sections as input ones.
+    *
+    * @param polySections the original sections
+    * @return the converted sections
+    */
+    static std::vector<std::unique_ptr<NodeSection>> convert(
+        std::vector<const NodeSection*>& polySections);
+
+
+private:
+
+    static std::size_t convertShellAndHoles(
+        std::vector<const NodeSection*>& sections,
+        std::size_t shellIndex,
+        std::vector<std::unique_ptr<NodeSection>>& convertedSections);
+
+    static std::vector<std::unique_ptr<NodeSection>> convertHoles(
+        std::vector<const NodeSection*>& sections);
+
+    static NodeSection* createSection(
+        const NodeSection* ns,
+        const CoordinateXY* v0,
+        const CoordinateXY* v1);
+
+    static std::vector<const NodeSection*> extractUnique(
+        std::vector<const NodeSection*>& sections);
+
+    static std::size_t next(
+        std::vector<const NodeSection *>& ns, std::size_t i);
+
+    static std::size_t findShell(
+        std::vector<const NodeSection *>& polySections);
+
+
+};
+
+} // namespace geos.operation.relateng
+} // namespace geos.operation
+} // namespace geos
+
diff --git a/include/geos/operation/relateng/RelateEdge.h b/include/geos/operation/relateng/RelateEdge.h
new file mode 100644
index 000000000..96afc244c
--- /dev/null
+++ b/include/geos/operation/relateng/RelateEdge.h
@@ -0,0 +1,176 @@
+/**********************************************************************
+ *
+ * GEOS - Geometry Engine Open Source
+ * http://geos.osgeo.org
+ *
+ * Copyright (c) 2024 Martin Davis
+ * Copyright (C) 2024 Paul Ramsey <pramsey at cleverelephant.ca>
+ *
+ * This is free software; you can redistribute and/or modify it under
+ * the terms of the GNU Lesser General Public Licence as published
+ * by the Free Software Foundation.
+ * See the COPYING file for more information.
+ *
+ **********************************************************************/
+
+#pragma once
+
+#include <geos/geom/Location.h>
+#include <geos/export.h>
+
+#include <memory>
+#include <vector>
+
+// Forward declarations
+namespace geos {
+namespace operation {
+namespace relateng {
+class RelateNode;
+}
+}
+namespace geom {
+    class CoordinateXY;
+    class Geometry;
+}
+}
+
+
+using geos::geom::CoordinateXY;
+using geos::geom::Geometry;
+using geos::geom::Location;
+
+
+namespace geos {      // geos.
+namespace operation { // geos.operation
+namespace relateng { // geos.operation.relateng
+
+
+class GEOS_DLL RelateEdge {
+
+private:
+
+    /**
+    * Indicates that the location is currently unknown
+    */
+    static constexpr Location LOC_UNKNOWN = Location::NONE;
+
+    // Members
+    const RelateNode* node;
+    const CoordinateXY* dirPt;
+
+    int aDim = DIM_UNKNOWN;
+    Location aLocLeft = LOC_UNKNOWN;
+    Location aLocRight = LOC_UNKNOWN;
+    Location aLocLine = LOC_UNKNOWN;
+
+    int bDim = DIM_UNKNOWN;
+    Location bLocLeft = LOC_UNKNOWN;
+    Location bLocRight = LOC_UNKNOWN;
+    Location bLocLine = LOC_UNKNOWN;
+
+
+public:
+
+    // Constants
+    static constexpr bool IS_FORWARD = true;
+    static constexpr bool IS_REVERSE = false;
+    static constexpr int DIM_UNKNOWN = -1;
+
+    // Constructors
+    RelateEdge(
+        const RelateNode* node, const CoordinateXY* pt,
+        bool isA, bool isForward);
+
+    RelateEdge(
+        const RelateNode* node, const CoordinateXY* pt,
+        bool isA);
+
+    RelateEdge(
+        const RelateNode* node, const CoordinateXY* pt,
+        bool isA, Location locLeft, Location locRight, Location locLine);
+
+    // Methods
+    static RelateEdge* create(
+        const RelateNode* node,
+        const CoordinateXY* dirPt,
+        bool isA, int dim, bool isForward);
+
+    static std::size_t findKnownEdgeIndex(
+        std::vector<std::unique_ptr<RelateEdge>>& edges,
+        bool isA);
+
+    static void setAreaInterior(
+        std::vector<std::unique_ptr<RelateEdge>>& edges,
+        bool isA);
+
+    bool isInterior(bool isA, int position) const;
+
+    Location location(bool isA, int position) const;
+
+    int compareToEdge(const CoordinateXY* edgeDirPt) const;
+
+    void setDimLocations(bool isA, int dim, Location loc);
+
+    void setAreaInterior(bool isA);
+
+    void setLocation(bool isA, int pos, Location loc);
+
+    void setAllLocations(bool isA, Location loc);
+
+    void setUnknownLocations(bool isA, Location loc);
+
+    void merge(bool isA, int dim, bool isForward);
+
+    std::string toString() const;
+
+    friend std::ostream& operator<<(std::ostream& os, const RelateEdge& re);
+
+
+private:
+
+    // Methods
+    void mergeSideLocation(bool isA, int pos, Location loc);
+
+    /**
+    * Area edges override Line edges.
+    * Merging edges of same dimension is a no-op for
+    * the dimension and on location.
+    * But merging an area edge into a line edge
+    * sets the dimension to A and the location to BOUNDARY.
+    *
+    * @param isA
+    * @param locEdge
+    */
+    void mergeDimEdgeLoc(bool isA, Location locEdge);
+
+    void setDimension(bool isA, int dimension);
+
+    void setLeft(bool isA, Location loc);
+
+    void setRight(bool isA, Location loc);
+
+    void setOn(bool isA, Location loc);
+
+    int dimension(bool isA) const;
+
+    bool isKnown(bool isA) const;
+
+    bool isKnown(bool isA, int pos) const;
+
+    void setLocations(bool isA, Location locLeft, Location locRight, Location locLine);
+
+    void setLocationsLine(bool isA);
+
+    void setLocationsArea(bool isA, bool isForward);
+
+    std::string labelString() const;
+
+    std::string locationString(bool isA) const;
+
+
+};
+
+} // namespace geos.operation.relateng
+} // namespace geos.operation
+} // namespace geos
+
diff --git a/include/geos/operation/relateng/RelateGeometry.h b/include/geos/operation/relateng/RelateGeometry.h
new file mode 100644
index 000000000..3c071d056
--- /dev/null
+++ b/include/geos/operation/relateng/RelateGeometry.h
@@ -0,0 +1,234 @@
+/**********************************************************************
+ *
+ * GEOS - Geometry Engine Open Source
+ * http://geos.osgeo.org
+ *
+ * Copyright (c) 2024 Martin Davis
+ * Copyright (C) 2024 Paul Ramsey <pramsey at cleverelephant.ca>
+ *
+ * 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/algorithm/BoundaryNodeRule.h>
+#include <geos/geom/Coordinate.h>
+#include <geos/geom/Dimension.h>
+#include <geos/geom/Location.h>
+#include <geos/operation/relateng/RelatePointLocator.h>
+#include <geos/operation/relateng/RelateSegmentString.h>
+#include <geos/export.h>
+
+#include <string>
+#include <sstream>
+
+
+// Forward declarations
+namespace geos {
+namespace geom {
+    class CoordinateSequence;
+    class Envelope;
+    class Geometry;
+    class LinearRing;
+    class LineString;
+    class MultiPolygon;
+    class Point;
+}
+namespace noding {
+    class SegmentString;
+}
+}
+
+
+namespace geos {      // geos.
+namespace operation { // geos.operation
+namespace relateng { // geos.operation.relateng
+
+using namespace geos::geom;
+using geos::algorithm::BoundaryNodeRule;
+using geos::noding::SegmentString;
+
+
+class GEOS_DLL RelateGeometry {
+
+private:
+
+    // Members
+
+    const Geometry* geom;
+    bool m_isPrepared = false;
+    const Envelope* geomEnv;
+    const BoundaryNodeRule& boundaryNodeRule;
+    int geomDim = Dimension::False;
+    bool isLineZeroLen = false;
+    bool isGeomEmpty = false;
+
+    Coordinate::ConstXYSet uniquePoints;
+    std::unique_ptr<RelatePointLocator> locator;
+    int elementId = 0;
+    bool hasPoints = false;
+    bool hasLines = false;
+    bool hasAreas = false;
+
+    /*
+     * Memory contexts for lower level allocations
+     */
+    std::vector<std::unique_ptr<const RelateSegmentString>> segStringTempStore;
+    std::vector<std::unique_ptr<const RelateSegmentString>> segStringPermStore;
+    std::vector<std::unique_ptr<CoordinateSequence>> csStore;
+
+
+    // Methods
+
+    void analyzeDimensions();
+
+    /**
+    * Tests if all geometry linear elements are zero-length.
+    * For efficiency the test avoids computing actual length.
+    *
+    * @param geom
+    * @return
+    */
+    static bool isZeroLength(const Geometry* geom);
+
+    static bool isZeroLength(const LineString* line);
+
+    RelatePointLocator* getLocator();
+
+    Coordinate::ConstXYSet createUniquePoints();
+
+    void extractSegmentStringsFromAtomic(bool isA,
+        const Geometry* geom, const MultiPolygon* parentPolygonal,
+        const Envelope* env,
+        std::vector<const SegmentString*>& segStrings,
+        std::vector<std::unique_ptr<const RelateSegmentString>>& segStore);
+
+    void extractRingToSegmentString(bool isA,
+        const LinearRing* ring, int ringId, const Envelope* env,
+        const Geometry* parentPoly,
+        std::vector<const SegmentString*>& segStrings,
+        std::vector<std::unique_ptr<const RelateSegmentString>>& segStore);
+
+    void extractSegmentStrings(bool isA,
+        const Envelope* env, const Geometry* geom,
+        std::vector<const SegmentString*>& segStrings,
+        std::vector<std::unique_ptr<const RelateSegmentString>>& segStore);
+
+    const CoordinateSequence* orientAndRemoveRepeated(
+        const CoordinateSequence* cs, bool orientCW);
+
+    const CoordinateSequence* removeRepeated(
+        const CoordinateSequence* cs);
+
+public:
+
+    static constexpr bool GEOM_A = true;
+    static constexpr bool GEOM_B = false;
+
+    RelateGeometry(const Geometry* input)
+        : RelateGeometry(input, false, BoundaryNodeRule::getBoundaryRuleMod2())
+        {};
+
+    RelateGeometry(const Geometry* input, const BoundaryNodeRule& bnRule)
+        : RelateGeometry(input, false, bnRule)
+        {};
+
+    RelateGeometry(const Geometry* input, bool p_isPrepared, const BoundaryNodeRule& bnRule);
+
+    static std::string name(bool isA);
+
+    const Geometry* getGeometry() const;
+
+    bool isPrepared() const;
+
+    const Envelope* getEnvelope() const;
+
+    int getDimension() const;
+
+    bool hasDimension(int dim) const;
+
+    /**
+    * Gets the actual non-empty dimension of the geometry.
+    * Zero-length LineStrings are treated as Points.
+    *
+    * @return the real (non-empty) dimension
+    */
+    int getDimensionReal() const;
+
+    bool hasEdges() const;
+
+    bool isNodeInArea(const CoordinateXY* nodePt, const Geometry* parentPolygonal);
+
+    Location locateLineEnd(const CoordinateXY* p) ;
+
+    /**
+     * Locates a vertex of a polygon.
+     * A vertex of a Polygon or MultiPolygon is on
+     * the {@link Location#BOUNDARY}.
+     * But a vertex of an overlapped polygon in a GeometryCollection
+     * may be in the {@link Location#INTERIOR}.
+     *
+     * @param pt the polygon vertex
+     * @return the location of the vertex
+     */
+    Location locateAreaVertex(const CoordinateXY* pt);
+
+    Location locateNode(const CoordinateXY* pt, const Geometry* parentPolygonal);
+
+    int locateWithDim(const CoordinateXY* pt);
+
+    bool isPointsOrPolygons() const;
+
+    /**
+     * Tests whether the geometry has polygonal topology.
+     * This is not the case if it is a GeometryCollection
+     * containing more than one polygon (since they may overlap
+     * or be adjacent).
+     * The significance is that polygonal topology allows more assumptions
+     * about the location of boundary vertices.
+     *
+     * @return true if the geometry has polygonal topology
+     */
+    bool isPolygonal() const;
+
+    bool isEmpty() const;
+
+    bool hasBoundary();
+
+    Coordinate::ConstXYSet& getUniquePoints();
+
+    std::vector<const Point*> getEffectivePoints();
+
+    /**
+     * Extract RSegmentStrings from the geometry which
+     * intersect a given envelope.
+     * If the envelope is null all edges are extracted.
+     * @param geomA
+     *
+     * @param env the envelope to extract around (may be null)
+     * @return a list of SegmentStrings
+     */
+    std::vector<const SegmentString*> extractSegmentStrings(bool isA, const Envelope* env);
+
+    std::string toString() const;
+
+    friend std::ostream& operator<<(std::ostream& os, const RelateGeometry& rg);
+
+    /**
+     * Disable copy construction and assignment. Needed to make this
+     * class compile under MSVC. (See https://stackoverflow.com/q/29565299)
+     * Classes with members that are vector<> of unique_ptr<> need this.
+     */
+    RelateGeometry(const RelateGeometry&) = delete;
+    RelateGeometry& operator=(const RelateGeometry&) = delete;
+
+};
+
+} // namespace geos.operation.relateng
+} // namespace geos.operation
+} // namespace geos
+
diff --git a/include/geos/operation/relateng/RelateMatrixPredicate.h b/include/geos/operation/relateng/RelateMatrixPredicate.h
new file mode 100644
index 000000000..b54ec2c3b
--- /dev/null
+++ b/include/geos/operation/relateng/RelateMatrixPredicate.h
@@ -0,0 +1,85 @@
+/**********************************************************************
+ *
+ * GEOS - Geometry Engine Open Source
+ * http://geos.osgeo.org
+ *
+ * Copyright (c) 2024 Martin Davis
+ * Copyright (C) 2024 Paul Ramsey <pramsey at cleverelephant.ca>
+ *
+ * This is free software; you can redistribute and/or modify it under
+ * the terms of the GNU Lesser General Public Licence as published
+ * by the Free Software Foundation.
+ * See the COPYING file for more information.
+ *
+ **********************************************************************/
+
+#pragma once
+
+#include <geos/geom/Location.h>
+#include <geos/geom/Dimension.h>
+#include <geos/geom/IntersectionMatrix.h>
+#include <geos/operation/relateng/IMPredicate.h>
+#include <geos/export.h>
+
+#include <memory>
+#include <vector>
+
+// Forward declarations
+namespace geos {
+namespace geom {
+    class Envelope;
+}
+}
+
+
+using geos::geom::Envelope;
+using geos::geom::Location;
+using geos::geom::Dimension;
+using geos::geom::IntersectionMatrix;
+
+
+namespace geos {      // geos.
+namespace operation { // geos.operation.
+namespace relateng {  // geos.operation.relateng
+
+
+class GEOS_DLL RelateMatrixPredicate : public IMPredicate {
+
+public:
+
+    RelateMatrixPredicate() {};
+
+    std::string name() const override {
+        return "relateMatrix";
+    };
+
+    bool requireInteraction() const override {
+        //-- ensure entire matrix is computed
+        return false;
+    };
+
+    bool isDetermined() const override {
+        //-- ensure entire matrix is computed
+        return false;
+    };
+
+    bool valueIM() override {
+        //-- indicates full matrix is being evaluated
+        return false;
+    };
+
+    /**
+    * Gets the current state of the IM matrix (which may only be partially complete).
+    *
+    * @return the IM matrix
+    */
+    std::unique_ptr<IntersectionMatrix> getIM() {
+        return std::unique_ptr<IntersectionMatrix>(new IntersectionMatrix(intMatrix));
+    }
+
+};
+
+} // namespace geos.operation.relateng
+} // namespace geos.operation
+} // namespace geos
+
diff --git a/include/geos/operation/relateng/RelateNG.h b/include/geos/operation/relateng/RelateNG.h
new file mode 100644
index 000000000..524c75085
--- /dev/null
+++ b/include/geos/operation/relateng/RelateNG.h
@@ -0,0 +1,282 @@
+/**********************************************************************
+ *
+ * GEOS - Geometry Engine Open Source
+ * http://geos.osgeo.org
+ *
+ * Copyright (c) 2024 Martin Davis
+ * Copyright (C) 2024 Paul Ramsey <pramsey at cleverelephant.ca>
+ *
+ * This is free software; you can redistribute and/or modify it under
+ * the terms of the GNU Lesser General Public Licence as published
+ * by the Free Software Foundation.
+ * See the COPYING file for more information.
+ *
+ **********************************************************************/
+
+#pragma once
+
+#include <geos/export.h>
+#include <geos/geom/Coordinate.h>
+#include <geos/noding/MCIndexSegmentSetMutualIntersector.h>
+#include <geos/operation/relateng/RelateGeometry.h>
+#include <string>
+#include <sstream>
+
+
+// Forward declarations
+namespace geos {
+namespace algorithm {
+    class BoundaryNodeRule;
+
+}
+namespace geom {
+    class Geometry;
+}
+namespace noding {
+}
+namespace operation {
+namespace relateng {
+    class TopologyPredicate;
+    class TopologyComputer;
+    class EdgeSegmentIntersector;
+}
+}
+}
+
+
+namespace geos {      // geos.
+namespace operation { // geos.operation
+namespace relateng { // geos.operation.relateng
+
+
+using geos::geom::CoordinateXY;
+using geos::geom::Geometry;
+using geos::algorithm::BoundaryNodeRule;
+using geos::noding::MCIndexSegmentSetMutualIntersector;
+
+
+/**
+ * Computes the value of topological predicates between two geometries based on the
+ * Dimensionally-Extended 9-Intersection Model <https://en.wikipedia.org/wiki/DE-9IM> (DE-9IM).
+ * Standard and custom topological predicates are provided by RelatePredicate.
+ *
+ * The RelateNG algorithm has the following capabilities:
+ *
+ *   * Efficient short-circuited evaluation of topological predicates
+ *     (including matching custom DE-9IM matrix patterns)
+ *   * Optimized repeated evaluation of predicates against a single geometry
+ *     via cached spatial indexes (AKA "prepared mode")
+ *   * Robust computation (only point-local topology is required,
+ *     so invalid geometry topology does not cause failures)
+ *   * GeometryCollection inputs containing mixed types and overlapping polygons
+ *     are supported, using union semantics.
+ *   * Zero-length LineStrings are treated as being topologically identical to Points.
+ *   * Support for BoundaryNodeRule.
+ *
+ * See IntersectionMatrixPattern for a description of DE-9IM patterns.
+ *
+ * If not specified, the standard BoundaryNodeRule::MOD2_BOUNDARY_RULE is used.
+ *
+ * RelateNG operates in 2D only; it ignores any Z ordinates.
+ *
+ * This implementation replaces RelateOp and PreparedGeometry.
+ *
+ * FUTURE WORK
+ *
+ *   * Support for a distance tolerance to provide "approximate" predicate evaluation
+ *
+ * @author Martin Davis
+ *
+ * @see RelateOp
+ * @see PreparedGeometry
+ */
+class GEOS_DLL RelateNG {
+
+private:
+
+    // Members
+    const BoundaryNodeRule& boundaryNodeRule;
+    RelateGeometry geomA;
+    std::unique_ptr<MCIndexSegmentSetMutualIntersector> edgeMutualInt = nullptr;
+
+    // Methods
+
+    RelateNG(const Geometry* inputA, bool isPrepared, const BoundaryNodeRule& bnRule)
+        : boundaryNodeRule(bnRule)
+        , geomA(inputA, isPrepared, bnRule)
+        {}
+
+    RelateNG(const Geometry* inputA, bool isPrepared)
+        : RelateNG(inputA, isPrepared, BoundaryNodeRule::getBoundaryRuleMod2())
+        {}
+
+    bool hasRequiredEnvelopeInteraction(const Geometry* b, TopologyPredicate& predicate);
+
+    bool finishValue(TopologyPredicate& predicate);
+
+    void computePP(RelateGeometry& geomB, TopologyComputer& topoComputer);
+
+    void computeAtPoints(RelateGeometry& geom, bool isA, RelateGeometry& geomTarget, TopologyComputer& topoComputer);
+
+    bool computePoints(RelateGeometry& geom, bool isA, RelateGeometry& geomTarget, TopologyComputer& topoComputer);
+
+    void computePoint(bool isA, const CoordinateXY* pt, RelateGeometry& geomTarget, TopologyComputer& topoComputer);
+
+    bool computeLineEnds(RelateGeometry& geom, bool isA, RelateGeometry& geomTarget, TopologyComputer& topoComputer);
+
+    bool computeLineEnd(RelateGeometry& geom, bool isA, const CoordinateXY* pt, RelateGeometry& geomTarget, TopologyComputer& topoComputer);
+
+    bool computeAreaVertex(RelateGeometry& geom, bool isA, RelateGeometry& geomTarget, TopologyComputer& topoComputer);
+
+    bool computeAreaVertex(RelateGeometry& geom, bool isA, const LinearRing* ring, RelateGeometry& geomTarget, TopologyComputer& topoComputer);
+
+    void computeAtEdges(RelateGeometry& geomB, TopologyComputer& topoComputer);
+
+    void computeEdgesAll(std::vector<const SegmentString*>& edgesB, const Envelope* envInt, EdgeSegmentIntersector& intersector);
+
+    void computeEdgesMutual(std::vector<const SegmentString*>& edgesB, const Envelope* envInt, EdgeSegmentIntersector& intersector);
+
+
+
+public:
+
+    /**
+     * Tests whether the topological relationship between two geometries
+     * satisfies a topological predicate.
+     *
+     * @param a the A input geometry
+     * @param b the B input geometry
+     * @param pred the topological predicate
+     * @return true if the topological relationship is satisfied
+     */
+    static bool relate(const Geometry* a, const Geometry* b, TopologyPredicate& pred);
+
+    /**
+     * Tests whether the topological relationship between two geometries
+     * satisfies a topological predicate,
+     * using a given BoundaryNodeRule.
+     *
+     * @param a the A input geometry
+     * @param b the B input geometry
+     * @param pred the topological predicate
+     * @param bnRule the Boundary Node Rule to use
+     * @return true if the topological relationship is satisfied
+     */
+    static bool relate(const Geometry* a, const Geometry* b, TopologyPredicate& pred, const BoundaryNodeRule& bnRule);
+
+    /**
+     * Tests whether the topological relationship to a geometry
+     * matches a DE-9IM matrix pattern.
+     *
+     * @param a the A input geometry
+     * @param b the B input geometry
+     * @param imPattern the DE-9IM pattern to match
+     * @return true if the geometries relationship matches the DE-9IM pattern
+     *
+     * @see IntersectionMatrixPattern
+     */
+    static bool relate(const Geometry* a, const Geometry* b, const std::string& imPattern);
+
+    /**
+     * Computes the DE-9IM matrix
+     * for the topological relationship between two geometries.
+     *
+     * @param a the A input geometry
+     * @param b the B input geometry
+     * @return the DE-9IM matrix for the topological relationship
+     */
+    static std::unique_ptr<IntersectionMatrix> relate(const Geometry* a, const Geometry* b);
+
+    /**
+     * Computes the DE-9IM matrix
+     * for the topological relationship between two geometries.
+     *
+     * @param a the A input geometry
+     * @param b the B input geometry
+     * @param bnRule the Boundary Node Rule to use
+     * @return the DE-9IM matrix for the relationship
+     */
+    static std::unique_ptr<IntersectionMatrix> relate(const Geometry* a, const Geometry* b, const BoundaryNodeRule& bnRule);
+
+    /**
+     * Creates a prepared RelateNG instance to optimize the
+     * evaluation of relationships against a single geometry.
+     *
+     * @param a the A input geometry
+     * @return a prepared instance
+     */
+    static std::unique_ptr<RelateNG> prepare(const Geometry* a);
+
+    /**
+     * Creates a prepared RelateNG instance to optimize the
+     * computation of predicates against a single geometry,
+     * using a given BoundaryNodeRule.
+     *
+     * @param a the A input geometry
+     * @param bnRule the required BoundaryNodeRule
+     * @return a prepared instance
+     */
+    static std::unique_ptr<RelateNG> prepare(const Geometry* a, const BoundaryNodeRule& bnRule);
+
+
+    /**
+     * Computes the DE-9IM matrix for the topological relationship to a geometry.
+     *
+     * @param b the B geometry to test against
+     * @return the DE-9IM matrix
+     */
+    std::unique_ptr<IntersectionMatrix> evaluate(const Geometry* b);
+
+
+    /**
+     * Tests whether the topological relationship to a geometry
+     * matches a DE-9IM matrix pattern.
+     *
+     * @param b the B geometry to test against
+     * @param imPattern the DE-9IM pattern to match
+     * @return true if the geometries' topological relationship matches the DE-9IM pattern
+     *
+     * @see IntersectionMatrixPattern
+     */
+    bool evaluate(const Geometry* b, const std::string& imPattern);
+
+    /**
+     * Tests whether the topological relationship to a geometry
+     * satisfies a topology predicate.
+     *
+     * @param b the B geometry to test against
+     * @param predicate the topological predicate
+     * @return true if the predicate is satisfied
+     */
+    bool evaluate(const Geometry* b, TopologyPredicate& predicate);
+
+    static bool intersects(const Geometry* a, const Geometry* b);
+    static bool crosses(const Geometry* a, const Geometry* b);
+    static bool disjoint(const Geometry* a, const Geometry* b);
+    static bool touches(const Geometry* a, const Geometry* b);
+    static bool within(const Geometry* a, const Geometry* b);
+    static bool contains(const Geometry* a, const Geometry* b);
+    static bool overlaps(const Geometry* a, const Geometry* b);
+    static bool covers(const Geometry* a, const Geometry* b);
+    static bool coveredBy(const Geometry* a, const Geometry* b);
+    static bool equalsTopo(const Geometry* a, const Geometry* b);
+
+    bool intersects(const Geometry* a);
+    bool crosses(const Geometry* a);
+    bool disjoint(const Geometry* a);
+    bool touches(const Geometry* a);
+    bool within(const Geometry* a);
+    bool contains(const Geometry* a);
+    bool overlaps(const Geometry* a);
+    bool covers(const Geometry* a);
+    bool coveredBy(const Geometry* a);
+    bool equalsTopo(const Geometry* a);
+    bool relate(const Geometry* a, const std::string& pat);
+    std::unique_ptr<IntersectionMatrix> relate(const Geometry* a);
+
+};
+
+} // namespace geos.operation.relateng
+} // namespace geos.operation
+} // namespace geos
+
diff --git a/include/geos/operation/relateng/RelateNode.h b/include/geos/operation/relateng/RelateNode.h
new file mode 100644
index 000000000..581eec359
--- /dev/null
+++ b/include/geos/operation/relateng/RelateNode.h
@@ -0,0 +1,146 @@
+/**********************************************************************
+ *
+ * GEOS - Geometry Engine Open Source
+ * http://geos.osgeo.org
+ *
+ * Copyright (c) 2024 Martin Davis
+ * Copyright (C) 2024 Paul Ramsey <pramsey at cleverelephant.ca>
+ *
+ * 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/operation/relateng/RelateEdge.h>
+
+#include <vector>
+#include <memory>
+#include <cassert>
+
+#include <geos/export.h>
+
+
+// Forward declarations
+namespace geos {
+namespace operation {
+namespace relateng {
+    class NodeSection;
+}
+}
+namespace geom {
+    class CoordinateXY;
+    class Geometry;
+}
+}
+
+
+using geos::geom::CoordinateXY;
+using geos::geom::Geometry;
+
+
+namespace geos {      // geos.
+namespace operation { // geos.operation
+namespace relateng { // geos.operation.relateng
+
+
+class GEOS_DLL RelateNode {
+
+private:
+
+    // Members
+
+    /**
+    * A list of the edges around the node in CCW order,
+    * ordered by their CCW angle with the positive X-axis.
+    */
+    std::vector<std::unique_ptr<RelateEdge>> edges;
+
+    const CoordinateXY* nodePt;
+
+
+    // Methods
+
+    void updateEdgesInArea(bool isA, std::size_t indexFrom, std::size_t indexTo);
+
+    void updateIfAreaPrev(bool isA, std::size_t index);
+
+    void updateIfAreaNext(bool isA, std::size_t index);
+
+    const RelateEdge* addLineEdge(bool isA, const CoordinateXY* dirPt);
+
+    const RelateEdge* addAreaEdge(bool isA, const CoordinateXY* dirPt, bool isForward);
+
+    /**
+    * Adds or merges an edge to the node.
+    *
+    * @param isA
+    * @param dirPt
+    * @param dim dimension of the geometry element containing the edge
+    * @param isForward the direction of the edge
+    *
+    * @return the created or merged edge for this point
+    */
+    const RelateEdge* addEdge(bool isA, const CoordinateXY* dirPt, int dim, bool isForward);
+
+    void finishNode(bool isA, bool isAreaInterior);
+
+    void propagateSideLocations(bool isA, std::size_t startIndex);
+
+    static std::size_t prevIndex(std::vector<std::unique_ptr<RelateEdge>>& list, std::size_t index);
+
+    static std::size_t nextIndex(std::vector<std::unique_ptr<RelateEdge>>& list, std::size_t i);
+
+    std::size_t indexOf(
+        const std::vector<std::unique_ptr<RelateEdge>>& edges,
+        const RelateEdge* edge) const;
+
+
+public:
+
+    RelateNode(const CoordinateXY* pt)
+        : nodePt(pt)
+        {};
+
+    const CoordinateXY* getCoordinate() const;
+
+    const std::vector<std::unique_ptr<RelateEdge>>& getEdges() const;
+
+    void addEdges(std::vector<const NodeSection *>& nss);
+    void addEdges(std::vector<std::unique_ptr<NodeSection>>& nss);
+
+    void addEdges(const NodeSection* ns);
+
+    /**
+    * Computes the final topology for the edges around this node.
+    * Although nodes lie on the boundary of areas or the interior of lines,
+    * in a mixed GC they may also lie in the interior of an area.
+    * This changes the locations of the sides and line to Interior.
+    *
+    * @param isAreaInteriorA true if the node is in the interior of A
+    * @param isAreaInteriorB true if the node is in the interior of B
+    */
+    void finish(bool isAreaInteriorA, bool isAreaInteriorB);
+
+    std::string toString() const;
+
+    bool hasExteriorEdge(bool isA);
+
+    friend std::ostream& operator<<(std::ostream& os, const RelateNode& ns);
+
+    /**
+     * Disable copy construction and assignment. Apparently needed to make this
+     * class compile under MSVC. (See https://stackoverflow.com/q/29565299)
+     */
+    RelateNode(const RelateNode&) = delete;
+    RelateNode& operator=(const RelateNode&) = delete;
+
+};
+
+} // namespace geos.operation.relateng
+} // namespace geos.operation
+} // namespace geos
+
diff --git a/include/geos/operation/relateng/RelatePointLocator.h b/include/geos/operation/relateng/RelatePointLocator.h
new file mode 100644
index 000000000..aab269c5a
--- /dev/null
+++ b/include/geos/operation/relateng/RelatePointLocator.h
@@ -0,0 +1,213 @@
+/**********************************************************************
+ *
+ * GEOS - Geometry Engine Open Source
+ * http://geos.osgeo.org
+ *
+ * Copyright (c) 2024 Martin Davis
+ * Copyright (C) 2024 Paul Ramsey <pramsey at cleverelephant.ca>
+ *
+ * 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/algorithm/BoundaryNodeRule.h>
+#include <geos/algorithm/locate/PointOnGeometryLocator.h>
+#include <geos/operation/relateng/AdjacentEdgeLocator.h>
+#include <geos/operation/relateng/LinearBoundary.h>
+#include <geos/geom/Coordinate.h>
+#include <geos/geom/Location.h>
+#include <geos/export.h>
+
+#include <memory>
+#include <vector>
+
+// Forward declarations
+namespace geos {
+namespace algorithm {
+    namespace locate {
+        // class PointOnGeometryLocator;
+    }
+}
+namespace operation {
+    namespace relateng {
+        // class LinearBoundary;
+        // class AdjacentEdgeLocator;
+    }
+}
+namespace geom {
+    class CoordinateXY;
+    class Geometry;
+    class LineString;
+    class Point;
+}
+}
+
+
+using geos::algorithm::BoundaryNodeRule;
+using geos::algorithm::locate::PointOnGeometryLocator;
+using geos::geom::Coordinate;
+using geos::geom::CoordinateXY;
+using geos::geom::Geometry;
+using geos::geom::LineString;
+using geos::geom::Point;
+using geos::geom::Location;
+
+
+namespace geos {      // geos.
+namespace operation { // geos.operation
+namespace relateng { // geos.operation.relateng
+
+
+/**
+ * Locates a point on a geometry, including mixed-type collections.
+ * The dimension of the containing geometry element is also determined.
+ * GeometryCollections are handled with union semantics;
+ * i.e. the location of a point is that location of that point
+ * on the union of the elements of the collection.
+ *
+ * Union semantics for GeometryCollections has the following behaviours:
+ *
+ *  * For a mixed-dimension (heterogeneous) collection
+ *    a point may lie on two geometry elements with different dimensions.
+ *    In this case the location on the largest-dimension element is reported.
+ *  * For a collection with overlapping or adjacent polygons,
+ *    points on polygon element boundaries may lie in the effective interior
+ *    of the collection geometry.
+ *
+ * Prepared mode is supported via cached spatial indexes.
+ *
+ * @author Martin Davis
+ */
+class GEOS_DLL RelatePointLocator {
+
+private:
+
+    // Members
+
+    const Geometry* geom;
+    bool isPrepared = false;
+    const BoundaryNodeRule& boundaryRule;
+    std::unique_ptr<AdjacentEdgeLocator> adjEdgeLocator;
+    Coordinate::ConstXYSet points;
+    std::vector<const LineString *> lines;
+    std::vector<const Geometry *> polygons;
+    std::vector<std::unique_ptr<PointOnGeometryLocator>> polyLocator;
+    std::unique_ptr<LinearBoundary> lineBoundary;
+    bool isEmpty;
+
+
+public:
+
+    // Constructors
+
+    RelatePointLocator(const Geometry* p_geom)
+        : RelatePointLocator(p_geom, false, BoundaryNodeRule::getBoundaryRuleMod2())
+        {};
+
+    RelatePointLocator(const Geometry* p_geom, bool p_isPrepared, const BoundaryNodeRule& p_bnRule)
+        : geom(p_geom)
+        , isPrepared(p_isPrepared)
+        , boundaryRule(p_bnRule)
+    {
+        init(geom);
+    };
+
+    void init(const Geometry* p_geom);
+
+    bool hasBoundary() const;
+
+    void extractElements(const Geometry* geom);
+
+    void addPoint(const Point* pt);
+
+    void addLine(const LineString* line);
+
+    void addPolygonal(const Geometry* polygonal);
+
+    Location locate(const CoordinateXY* p);
+
+    Location locateLineEnd(const CoordinateXY* p) const;
+
+    /*
+    * Locates a point which is known to be a node of the geometry
+    * (i.e. a point or on an edge).
+    *
+    * @param p the node point to locate
+    * @param parentPolygonal the polygon the point is a node of
+    * @return the location of the node point
+    */
+    Location locateNode(const CoordinateXY* p, const Geometry* parentPolygonal);
+
+    /**
+    * Locates a point which is known to be a node of the geometry,
+    * as a DimensionLocation.
+    *
+    * @param p the point to locate
+    * @param parentPolygonal the polygon the point is a node of
+    * @return the dimension and location of the point
+    */
+    int locateNodeWithDim(const CoordinateXY* p, const Geometry* parentPolygonal);
+
+    /**
+    * Computes the topological location ( Location) of a single point
+    * in a Geometry, as well as the dimension of the geometry element the point
+    * is located in (if not in the Exterior).
+    * It handles both single-element and multi-element Geometries.
+    * The algorithm for multi-part Geometries
+    * takes into account the SFS Boundary Determination Rule.
+    *
+    * @param p the point to locate
+    * @return the Location of the point relative to the input Geometry
+    */
+    int locateWithDim(const CoordinateXY* p);
+
+
+private:
+
+    // Methods
+
+    /**
+    * Computes the topological location (Location) of a single point
+    * in a Geometry, as well as the dimension of the geometry element the point
+    * is located in (if not in the Exterior).
+    * It handles both single-element and multi-element Geometries.
+    * The algorithm for multi-part Geometries
+    * takes into account the SFS Boundary Determination Rule.
+    *
+    * @param p the coordinate to locate
+    * @param isNode whether the coordinate is a node (on an edge) of the geometry
+    * @param polygon
+    * @return the Location of the point relative to the input Geometry
+    */
+    int locateWithDim(const CoordinateXY* p, bool isNode, const Geometry* parentPolygonal);
+
+    int computeDimLocation(const CoordinateXY* p, bool isNode, const Geometry* parentPolygonal);
+
+    Location locateOnPoints(const CoordinateXY* p) const;
+
+    Location locateOnLines(const CoordinateXY* p, bool isNode);
+
+    Location locateOnLine(const CoordinateXY* p, /*bool isNode,*/ const LineString* l);
+
+    Location locateOnPolygons(const CoordinateXY* p, bool isNode, const Geometry* parentPolygonal);
+
+    Location locateOnPolygonal(const CoordinateXY* p,
+        bool isNode,
+        const Geometry* parentPolygonal,
+        std::size_t index);
+
+    PointOnGeometryLocator * getLocator(std::size_t index);
+
+
+
+};
+
+} // namespace geos.operation.relateng
+} // namespace geos.operation
+} // namespace geos
+
diff --git a/include/geos/operation/relateng/RelatePredicate.h b/include/geos/operation/relateng/RelatePredicate.h
new file mode 100644
index 000000000..ca1db412f
--- /dev/null
+++ b/include/geos/operation/relateng/RelatePredicate.h
@@ -0,0 +1,652 @@
+/**********************************************************************
+ *
+ * GEOS - Geometry Engine Open Source
+ * http://geos.osgeo.org
+ *
+ * Copyright (c) 2024 Martin Davis
+ * Copyright (C) 2024 Paul Ramsey <pramsey at cleverelephant.ca>
+ *
+ * This is free software; you can redistribute and/or modify it under
+ * the terms of the GNU Lesser General Public Licence as published
+ * by the Free Software Foundation.
+ * See the COPYING file for more information.
+ *
+ **********************************************************************/
+
+#pragma once
+
+#include <geos/geom/Location.h>
+#include <geos/operation/relateng/BasicPredicate.h>
+#include <geos/operation/relateng/IMPatternMatcher.h>
+#include <geos/operation/relateng/IMPredicate.h>
+#include <geos/operation/relateng/RelateGeometry.h>
+#include <geos/geom/Envelope.h>
+#include <geos/export.h>
+
+
+using geos::geom::Envelope;
+using geos::geom::Location;
+
+
+namespace geos {      // geos.
+namespace operation { // geos.operation.
+namespace relateng {  // geos.operation.relateng
+
+
+class GEOS_DLL RelatePredicate {
+
+public:
+
+/************************************************************************
+ *
+ * Creates a predicate to determine whether two geometries intersect.
+ *
+ * The intersects predicate has the following equivalent definitions:
+ *
+ *  * The two geometries have at least one point in common
+ *  * The DE-9IM Intersection Matrix for the two geometries matches
+ *    at least one of the patterns
+ *
+ *    [T********]
+ *    [*T*******]
+ *    [***T*****]
+ *    [****T****]
+ *
+ *  disjoint() = false
+ *  (intersects is the inverse of disjoint)
+ *
+ * @return the predicate instance
+ *
+ * @see disjoint()
+ */
+class IntersectsPredicate : public BasicPredicate {
+
+public:
+
+    std::string name() const override {
+        return std::string("intersects");
+    }
+
+    bool requireSelfNoding() const override {
+        //-- self-noding is not required to check for a simple interaction
+        return false;
+    }
+
+    bool requireExteriorCheck(bool isSourceA) const override {
+        (void)isSourceA;
+        //-- intersects only requires testing interaction
+        return false;
+    }
+
+    void init(const Envelope& envA, const Envelope& envB) override {
+        require(envA.intersects(envB));
+    }
+
+    void updateDimension(Location locA, Location locB, int dimension) override {
+        (void)dimension;
+        setValueIf(true, isIntersection(locA, locB));
+    }
+
+    void finish() override {
+        //-- if no intersecting locations were found
+        setValue(false);
+    }
+
+};
+
+static std::unique_ptr<BasicPredicate> intersects();
+
+/************************************************************************
+ *
+ * Creates a predicate to determine whether two geometries are disjoint.
+ *
+ * The disjoint predicate has the following equivalent definitions:
+ *
+ *   * The two geometries have no point in common
+ *   * The DE-9IM Intersection Matrix for the two geometries matches
+ *        [FF*FF****]
+ *   * intersects() = false
+ *     (disjoint is the inverse of intersects)
+ *
+ * @return the predicate instance
+ *
+ * @see intersects()
+ */
+class DisjointPredicate : public BasicPredicate {
+
+    std::string name() const override {
+        return std::string("disjoint");
+    }
+
+    bool requireSelfNoding() const override {
+        //-- self-noding is not required to check for a simple interaction
+        return false;
+    }
+
+    bool requireInteraction() const override {
+        //-- ensure entire matrix is computed
+        return false;
+    }
+
+    bool requireExteriorCheck(bool isSourceA) const override {
+        (void)isSourceA;
+        //-- intersects only requires testing interaction
+        return false;
+    }
+
+    void init(const Envelope& envA, const Envelope& envB) override {
+        setValueIf(true, envA.disjoint(envB));
+    }
+
+    void updateDimension(Location locA, Location locB, int dimension) override {
+        (void)dimension;
+        setValueIf(false, isIntersection(locA, locB));
+    }
+
+    void finish() override {
+        //-- if no intersecting locations were found
+        setValue(true);
+    }
+};
+
+static std::unique_ptr<BasicPredicate> disjoint();
+
+/************************************************************************
+ * Creates a predicate to determine whether a geometry contains another geometry.
+ *
+ * The contains predicate has the following equivalent definitions:
+ *
+ *   * Every point of the other geometry is a point of this geometry,
+ *     and the interiors of the two geometries have at least one point in common.
+ *   * The DE-9IM Intersection Matrix for the two geometries matches
+ *     the pattern
+ *       [T*****FF*]
+ *   * within(B, A) = true
+ *     (contains is the converse of within)
+ *
+ * An implication of the definition is that "Geometries do not
+ * contain their boundary".  In other words, if a geometry A is a subset of
+ * the points in the boundary of a geometry B, B.contains(A) = false.
+ * (As a concrete example, take A to be a LineString which lies in the boundary of a Polygon B.)
+ * For a predicate with similar behavior but avoiding
+ * this subtle limitation, see covers().
+ *
+ * @return the predicate instance
+ *
+ * @see within()
+ */
+class ContainsPredicate : public IMPredicate {
+
+    std::string name() const override {
+        return std::string("contains");
+    }
+
+    bool requireCovers(bool isSourceA) override {
+        return isSourceA == RelateGeometry::GEOM_A;
+    }
+
+    bool requireExteriorCheck(bool isSourceA) const override {
+        //-- only need to check B against Exterior of A
+        return isSourceA == RelateGeometry::GEOM_B;
+    }
+
+    void init(int _dimA, int _dimB) override {
+        IMPredicate::init(_dimA, _dimB);
+        require(isDimsCompatibleWithCovers(dimA, dimB));
+    }
+
+    void init(const Envelope& envA, const Envelope& envB) override {
+        BasicPredicate::requireCovers(envA, envB);
+    }
+
+    bool isDetermined() const override {
+        return intersectsExteriorOf(RelateGeometry::GEOM_A);
+    }
+
+    bool valueIM() override {
+        return intMatrix.isContains();
+    }
+};
+
+static std::unique_ptr<IMPredicate> contains();
+
+
+
+/************************************************************************
+ * Creates a predicate to determine whether a geometry is within another geometry.
+ *
+ * The within predicate has the following equivalent definitions:
+ *
+ *   * Every point of this geometry is a point of the other geometry,
+ *     and the interiors of the two geometries have at least one point in common.
+ *   * The DE-9IM Intersection Matrix for the two geometries matches
+ *     [T*F**F***]
+ *   *  contains(B, A) = true
+ *      (within is the converse of  contains())
+ *
+ * An implication of the definition is that
+ * "The boundary of a Geometry is not within the Geometry".
+ * In other words, if a geometry A is a subset of
+ * the points in the boundary of a geometry B, within(B, A) = false
+ * (As a concrete example, take A to be a LineString which lies in the boundary of a Polygon B.)
+ * For a predicate with similar behavior but avoiding
+ * this subtle limitation, see coveredimBy().
+ *
+ * @return the predicate instance
+ *
+ * @see #contains()
+ */
+class WithinPredicate : public IMPredicate {
+
+    std::string name() const override {
+        return std::string("within");
+    }
+
+    bool requireCovers(bool isSourceA) override {
+        return isSourceA == RelateGeometry::GEOM_B;
+    }
+
+    bool requireExteriorCheck(bool isSourceA) const override {
+        //-- only need to check B against Exterior of A
+        return isSourceA == RelateGeometry::GEOM_A;
+    }
+
+    void init(int _dimA, int _dimB) override {
+        IMPredicate::init(_dimA, _dimB);
+        require(isDimsCompatibleWithCovers(dimB, dimA));
+    }
+
+    void init(const Envelope& envA, const Envelope& envB) override {
+        BasicPredicate::requireCovers(envB, envA);
+    }
+
+    bool isDetermined() const override {
+        return intersectsExteriorOf(RelateGeometry::GEOM_B);
+    }
+
+    bool valueIM() override {
+        return intMatrix.isWithin();
+    }
+};
+
+static std::unique_ptr<IMPredicate> within();
+
+
+
+/************************************************************************
+ * Creates a predicate to determine whether a geometry covers another geometry.
+ *
+ * The covers predicate has the following equivalent definitions:
+ *
+ * Every point of the other geometry is a point of this geometry.
+ * The DE-9IM Intersection Matrix for the two geometries matches
+ * at least one of the following patterns:
+ *
+ *  * [T*****FF*]
+ *  * [*T****FF*]
+ *  * [***T**FF*]
+ *  * [****T*FF*]
+ *
+ * coveredimBy(b, a) = true
+ * (covers is the converse of coveredimBy())
+ *
+ * If either geometry is empty, the value of this predicate is false.
+ *
+ * This predicate is similar to contains(),
+ * but is more inclusive (i.e. returns true for more cases).
+ * In particular, unlike contains it does not distinguish between
+ * points in the boundary and in the interior of geometries.
+ * For most cases, covers should be used in preference to contains.
+ * As an added benefit, covers is more amenable to optimization,
+ * and hence should be more performant.
+ *
+ * @return the predicate instance
+ *
+ * @see #coveredimBy()
+ */
+class CoversPredicate : public IMPredicate {
+
+    std::string name() const override {
+        return std::string("covers");
+    }
+
+    bool requireCovers(bool isSourceA) override {
+        return isSourceA == RelateGeometry::GEOM_A;
+    }
+
+    bool requireExteriorCheck(bool isSourceA) const override {
+        //-- only need to check B against Exterior of A
+        return isSourceA == RelateGeometry::GEOM_B;
+    }
+
+    void init(int _dimA, int _dimB) override {
+        IMPredicate::init(_dimA, _dimB);
+        require(isDimsCompatibleWithCovers(dimA, dimB));
+    }
+
+    void init(const Envelope& envA, const Envelope& envB) override {
+        BasicPredicate::requireCovers(envA, envB);
+
+    }
+
+    bool isDetermined() const override {
+        return intersectsExteriorOf(RelateGeometry::GEOM_A);
+    }
+
+    bool valueIM() override {
+        return intMatrix.isCovers();
+    }
+};
+
+static std::unique_ptr<IMPredicate> covers();
+
+
+/************************************************************************
+* Creates a predicate to determine whether a geometry is covered
+* by another geometry.
+*
+* The coveredimBy predicate has the following equivalent definitions:
+*
+* Every point of this geometry is a point of the other geometry.
+* The DE-9IM Intersection Matrix for the two geometries matches
+* at least one of the following patterns:
+*
+*   [T*F**F***]
+*   [*TF**F***]
+*   [**FT*F***]
+*   [**F*TF***]
+*
+* covers(B, A) = true
+* (coveredimBy is the converse of covers())
+*
+* If either geometry is empty, the value of this predicate is false.
+*
+* This predicate is similar to within(),
+* but is more inclusive (i.e. returns true for more cases).
+*
+* @return the predicate instance
+*
+* @see #covers()
+*/
+class CoveredByPredicate : public IMPredicate {
+
+    std::string name() const override {
+        return std::string("coveredBy");
+    }
+
+    bool requireCovers(bool isSourceA) override {
+        return isSourceA == RelateGeometry::GEOM_B;
+    }
+
+    bool requireExteriorCheck(bool isSourceA) const override {
+        //-- only need to check B against Exterior of A
+        return isSourceA == RelateGeometry::GEOM_A;
+    }
+
+    void init(int _dimA, int _dimB) override {
+        IMPredicate::init(_dimA, _dimB);
+        require(isDimsCompatibleWithCovers(dimB, dimA));
+    }
+
+    void init(const Envelope& envA, const Envelope& envB) override {
+        BasicPredicate::requireCovers(envB, envA);
+    }
+
+    bool isDetermined() const override {
+        return intersectsExteriorOf(RelateGeometry::GEOM_B);
+    }
+
+    bool valueIM() override {
+        return intMatrix.isCoveredBy();
+    }
+
+};
+
+static std::unique_ptr<IMPredicate> coveredBy();
+
+
+/************************************************************************
+* Creates a predicate to determine whether a geometry crosses another geometry.
+*
+* The crosses predicate has the following equivalent definitions:
+*
+* The geometries have some but not all interior points in common.
+* The DE-9IM Intersection Matrix for the two geometries matches
+* one of the following patterns:
+*
+*    [T*T******] (for P/L, P/A, and L/A cases)
+*    [T*****T**] (for L/P, A/P, and A/L cases)
+*    [0********] (for L/L cases)
+*
+*
+* For the A/A and P/P cases this predicate returns false.
+*
+* The SFS defined this predicate only for P/L, P/A, L/L, and L/A cases.
+* To make the relation symmetric
+* JTS extends the definition to apply to L/P, A/P and A/L cases as well.
+*
+* @return the predicate instance
+*/
+
+class CrossesPredicate : public IMPredicate {
+
+    std::string name() const override {
+        return std::string("crosses");
+    }
+
+    void init(int _dimA, int _dimB) override {
+        IMPredicate::init(_dimA, _dimB);
+        bool isBothPointsOrAreas =
+            (dimA == Dimension::P && dimB == Dimension::P) ||
+            (dimA == Dimension::A && dimB == Dimension::A);
+        require(!isBothPointsOrAreas);
+    }
+
+    bool isDetermined() const override {
+        if (dimA == Dimension::L && dimB == Dimension::L) {
+            //-- L/L interaction can only be dim = P
+            if (getDimension(Location::INTERIOR, Location::INTERIOR) > Dimension::P)
+                return true;
+        }
+        else if (dimA < dimB) {
+            if (isIntersects(Location::INTERIOR, Location::INTERIOR) &&
+                isIntersects(Location::INTERIOR, Location::EXTERIOR)) {
+                return true;
+            }
+        }
+        else if (dimA > dimB) {
+            if (isIntersects(Location::INTERIOR, Location::INTERIOR) &&
+                isIntersects(Location::EXTERIOR, Location::INTERIOR)) {
+                return true;
+            }
+        }
+        return false;
+    }
+
+    bool valueIM() override {
+        return intMatrix.isCrosses(dimA, dimB);
+    }
+};
+
+static std::unique_ptr<IMPredicate> crosses();
+
+
+/************************************************************************
+* Creates a predicate to determine whether two geometries are
+* topologically equal.
+*
+* The equals predicate has the following equivalent definitions:
+*
+* The two geometries have at least one point in common,
+* and no point of either geometry lies in the exterior of the other geometry.
+* The DE-9IM Intersection Matrix for the two geometries matches
+* the pattern T*F**FFF*
+*
+* @return the predicate instance
+*/
+class EqualsTopoPredicate : public IMPredicate {
+
+    std::string name() const override {
+        return std::string("equals");
+    }
+
+    bool requireInteraction() const override {
+        //-- allow EMPTY = EMPTY
+        return false;
+    };
+
+    void init(int _dimA, int _dimB) override {
+        IMPredicate::init(_dimA, _dimB);
+        //-- don't require equal dims, because EMPTY = EMPTY for all dims
+    }
+
+    void init(const Envelope& envA, const Envelope& envB) override {
+        //-- handle EMPTY = EMPTY cases
+        setValueIf(true, envA.isNull() && envB.isNull());
+
+        require(envA.equals(&envB));
+    }
+
+    bool isDetermined() const override {
+        bool isEitherExteriorIntersects =
+            isIntersects(Location::INTERIOR, Location::EXTERIOR) ||
+            isIntersects(Location::BOUNDARY, Location::EXTERIOR) ||
+            isIntersects(Location::EXTERIOR, Location::INTERIOR) ||
+            isIntersects(Location::EXTERIOR, Location::BOUNDARY);
+
+        return isEitherExteriorIntersects;
+    }
+
+    bool valueIM() override {
+        return intMatrix.isEquals(dimA, dimB);
+    }
+
+};
+
+static std::unique_ptr<IMPredicate> equalsTopo();
+
+
+/************************************************************************
+ * Creates a predicate to determine whether a geometry overlaps another geometry.
+ *
+ * The overlaps predicate has the following equivalent definitions:
+ *
+ * The geometries have at least one point each not shared by the other
+ *     (or equivalently neither covers the other),
+ *     they have the same dimension,
+ *     and the intersection of the interiors of the two geometries has
+ *     the same dimension as the geometries themselves.
+ * The DE-9IM Intersection Matrix for the two geometries matches
+ *     [T*T***T**] (for P/P and A/A cases)
+ *     or [1*T***T**] (for L/L cases)
+ *
+ * If the geometries are of different dimension this predicate returns false.
+ * This predicate is symmetric.
+ *
+ * @return the predicate instance
+ */
+class OverlapsPredicate : public IMPredicate {
+
+    std::string name() const override {
+        return std::string("overlaps");
+    }
+
+    void init(int _dimA, int _dimB) override {
+        IMPredicate::init(_dimA, _dimB);
+        require(dimA == dimB);
+    }
+
+    bool isDetermined() const override {
+        if (dimA == Dimension::A || dimA == Dimension::P) {
+            if (isIntersects(Location::INTERIOR, Location::INTERIOR) &&
+                isIntersects(Location::INTERIOR, Location::EXTERIOR) &&
+                isIntersects(Location::EXTERIOR, Location::INTERIOR))
+            return true;
+        }
+        if (dimA == Dimension::L) {
+            if (isDimension(Location::INTERIOR, Location::INTERIOR, Dimension::L) &&
+                isIntersects(Location::INTERIOR, Location::EXTERIOR) &&
+                isIntersects(Location::EXTERIOR, Location::INTERIOR))
+            return true;
+        }
+        return false;
+    }
+
+    bool valueIM() override {
+        return intMatrix.isOverlaps(dimA, dimB);
+    }
+};
+
+static std::unique_ptr<IMPredicate> overlaps();
+
+
+
+
+
+/************************************************************************
+* Creates a predicate to determine whether a geometry touches another geometry.
+*
+* The touches predicate has the following equivalent definitions:
+*
+* The geometries have at least one point in common,
+* but their interiors do not intersect.
+* The DE-9IM Intersection Matrix for the two geometries matches
+* at least one of the following patterns
+*
+*   [FT*******]
+*   [F**T*****]
+*   [F***T****]
+*
+*
+* If both geometries have dimension 0, the predicate returns false,
+* since points have only interiors.
+* This predicate is symmetric.
+*
+* @return the predicate instance
+*/
+class TouchesPredicate : public IMPredicate {
+
+    std::string name() const override {
+        return std::string("touches");
+    }
+
+    void init(int _dimA, int _dimB) override {
+        IMPredicate::init(_dimA, _dimB);
+        bool isBothPoints = (dimA == 0 && dimB == 0);
+        require(! isBothPoints);
+    }
+
+    bool isDetermined() const override {
+        bool isInteriorsIntersects = isIntersects(Location::INTERIOR, Location::INTERIOR);
+        return isInteriorsIntersects;
+    }
+
+    bool valueIM() override {
+        return intMatrix.isTouches(dimA, dimB);
+    }
+};
+
+static std::unique_ptr<IMPredicate> touches();
+
+/**
+ * Creates a predicate that matches a DE-9IM matrix pattern.
+ *
+ * @param imPattern the pattern to match
+ * @return a predicate that matches the pattern
+ *
+ * @see IntersectionMatrixPattern
+ */
+static std::unique_ptr<TopologyPredicate> matches(const std::string& imPattern)
+{
+    return std::unique_ptr<TopologyPredicate>(new IMPatternMatcher(imPattern));
+}
+
+
+
+}; // !RelatePredicate
+
+
+} // namespace geos.operation.relateng
+} // namespace geos.operation
+} // namespace geos
+
diff --git a/include/geos/operation/relateng/RelateSegmentString.h b/include/geos/operation/relateng/RelateSegmentString.h
new file mode 100644
index 000000000..556ac4d37
--- /dev/null
+++ b/include/geos/operation/relateng/RelateSegmentString.h
@@ -0,0 +1,155 @@
+/**********************************************************************
+ *
+ * GEOS - Geometry Engine Open Source
+ * http://geos.osgeo.org
+ *
+ * Copyright (c) 2024 Martin Davis
+ * Copyright (C) 2024 Paul Ramsey <pramsey at cleverelephant.ca>
+ *
+ * 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/noding/BasicSegmentString.h>
+#include <geos/export.h>
+
+
+#include <string>
+#include <sstream>
+
+
+// Forward declarations
+namespace geos {
+namespace geom {
+    class CoordinateXY;
+    class CoordinateSequence;
+    class Geometry;
+}
+namespace operation {
+namespace relateng {
+    class RelateGeometry;
+    class NodeSection;
+}
+}
+}
+
+
+using geos::noding::BasicSegmentString;
+using geos::geom::Geometry;
+using geos::geom::CoordinateXY;
+using geos::geom::CoordinateSequence;
+
+
+namespace geos {      // geos.
+namespace operation { // geos.operation
+namespace relateng { // geos.operation.relateng
+
+
+
+class GEOS_DLL RelateSegmentString : public BasicSegmentString {
+
+private:
+
+    // Members
+    bool m_isA;
+    int m_dimension;
+    int m_id;
+    int m_ringId;
+    const RelateGeometry* m_inputGeom;
+    const Geometry* m_parentPolygonal = nullptr;
+
+    // Constructor
+    RelateSegmentString(
+        const CoordinateSequence* pts,
+        bool isA,
+        int dimension,
+        int id,
+        int ringId,
+        const Geometry* poly,
+        const RelateGeometry* inputGeom)
+        : BasicSegmentString(const_cast<CoordinateSequence*>(pts), nullptr)
+        , m_isA(isA)
+        , m_dimension(dimension)
+        , m_id(id)
+        , m_ringId(ringId)
+        , m_inputGeom(inputGeom)
+        , m_parentPolygonal(poly)
+        {}
+
+
+    // Methods
+
+    static const RelateSegmentString* createSegmentString(
+        const CoordinateSequence* pts,
+        bool isA, int dim, int elementId, int ringId,
+        const Geometry* poly, const RelateGeometry* parent);
+
+    /**
+     *
+     * @param ss
+     * @param segIndex
+     * @param pt
+     * @return the previous vertex, or null if none exists
+     */
+    const CoordinateXY* prevVertex(
+        std::size_t segIndex,
+        const CoordinateXY* pt) const;
+
+    /**
+     * @param ss
+     * @param segIndex
+     * @param pt
+     * @return the next vertex, or null if none exists
+     */
+    const CoordinateXY* nextVertex(
+        std::size_t segIndex,
+        const CoordinateXY* pt) const;
+
+
+public:
+
+    static const RelateSegmentString* createLine(
+        const CoordinateSequence* pts,
+        bool isA, int elementId,
+        const RelateGeometry* parent);
+
+    static const RelateSegmentString* createRing(
+        const CoordinateSequence* pts,
+        bool isA, int elementId, int ringId,
+        const Geometry* poly, const RelateGeometry* parent);
+
+    bool isA() const;
+
+    const RelateGeometry* getGeometry() const;
+
+    const Geometry* getPolygonal() const;
+
+    NodeSection* createNodeSection(std::size_t segIndex, const CoordinateXY intPt) const;
+
+    /**
+     * Tests if a segment intersection point has that segment as its
+     * canonical containing segment.
+     * Segments are half-closed, and contain their start point but not the endpoint,
+     * except for the final segment in a non-closed segment string, which contains
+     * its endpoint as well.
+     * This test ensures that vertices are assigned to a unique segment in a segment string.
+     * In particular, this avoids double-counting intersections which lie exactly
+     * at segment endpoints.
+     *
+     * @param segIndex the segment the point may lie on
+     * @param pt the point
+     * @return true if the segment contains the point
+     */
+    bool isContainingSegment(std::size_t segIndex, const CoordinateXY* pt) const;
+
+};
+
+} // namespace geos.operation.relateng
+} // namespace geos.operation
+} // namespace geos
+
diff --git a/include/geos/operation/relateng/TopologyComputer.h b/include/geos/operation/relateng/TopologyComputer.h
new file mode 100644
index 000000000..cad2a1bc3
--- /dev/null
+++ b/include/geos/operation/relateng/TopologyComputer.h
@@ -0,0 +1,226 @@
+/**********************************************************************
+ *
+ * GEOS - Geometry Engine Open Source
+ * http://geos.osgeo.org
+ *
+ * Copyright (c) 2024 Martin Davis
+ * Copyright (C) 2024 Paul Ramsey <pramsey at cleverelephant.ca>
+ *
+ * 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/operation/relateng/NodeSections.h>
+#include <geos/geom/Coordinate.h>
+#include <geos/geom/Location.h>
+#include <geos/export.h>
+
+// Forward declarations
+namespace geos {
+namespace operation {
+namespace relateng {
+    class NodeSection;
+    class RelateGeometry;
+    class RelateNode;
+    class TopologyPredicate;
+}
+}
+}
+
+
+using geos::geom::CoordinateXY;
+using geos::geom::Location;
+using geos::operation::relateng::NodeSection;
+using geos::operation::relateng::NodeSections;
+using geos::operation::relateng::RelateGeometry;
+using geos::operation::relateng::RelateNode;
+using geos::operation::relateng::TopologyPredicate;
+
+
+namespace geos {      // geos.
+namespace operation { // geos.operation
+namespace relateng { // geos.operation.relateng
+
+
+class GEOS_DLL TopologyComputer {
+
+private:
+
+    // Members
+    TopologyPredicate& predicate;
+    RelateGeometry& geomA;
+    RelateGeometry& geomB;
+    std::map<CoordinateXY, NodeSections*> nodeMap;
+    std::deque<std::unique_ptr<NodeSections>> nodeSectionsStore;
+
+    // Methods
+
+    /**
+    * Determine a priori partial EXTERIOR topology based on dimensions.
+    */
+    void initExteriorDims();
+
+    void initExteriorEmpty(bool geomNonEmpty);
+
+    RelateGeometry& getGeometry(bool isA) const;
+
+    void updateDim(Location locA, Location locB, int dimension);
+
+    void updateDim(bool isAB, Location loc1, Location loc2, int dimension);
+
+    /**
+     * Update topology for an intersection between A and B.
+     *
+     * @param a the section for geometry A
+     * @param b the section for geometry B
+     */
+    void updateIntersectionAB(const NodeSection* a, const NodeSection* b);
+
+    /**
+     * Updates topology for an AB Area-Area crossing node.
+     * Sections cross at a node if (a) the intersection is proper
+     * (i.e. in the interior of two segments)
+     * or (b) if non-proper then whether the linework crosses
+     * is determined by the geometry of the segments on either side of the node.
+     * In these situations the area geometry interiors intersect (in dimension 2).
+     *
+     * @param a the section for geometry A
+     * @param b the section for geometry B
+     */
+    void updateAreaAreaCross(const NodeSection* a, const NodeSection* b);
+
+    /**
+     * Updates topology for a node at an AB edge intersection.
+     *
+     * @param a the section for geometry A
+     * @param b the section for geometry B
+     */
+    void updateNodeLocation(const NodeSection* a, const NodeSection* b);
+
+    void addNodeSections(NodeSection* ns0, NodeSection* ns1);
+
+    void addLineEndOnPoint(bool isLineA, Location locLineEnd, Location locPoint, const CoordinateXY* pt);
+
+    void addLineEndOnLine(bool isLineA, Location locLineEnd, Location locLine, const CoordinateXY* pt);
+
+    void addLineEndOnArea(bool isLineA, Location locLineEnd, Location locArea, const CoordinateXY* pt);
+
+    /**
+     * Updates topology for an area vertex (in Interior or on Boundary)
+     * intersecting a point.
+     * Note that because the largest dimension of intersecting target is determined,
+     * the intersecting point is not part of any other target geometry,
+     * and hence its neighbourhood is in the Exterior of the target.
+     *
+     * @param isAreaA whether the area is the A input
+     * @param locArea the location of the vertex in the area
+     * @param pt the point at which topology is being updated
+     */
+    void addAreaVertexOnPoint(bool isAreaA, Location locArea, const CoordinateXY* pt);
+
+    void addAreaVertexOnLine(bool isAreaA, Location locArea, Location locTarget, const CoordinateXY* pt);
+
+    void evaluateNode(NodeSections* nodeSections);
+
+    void evaluateNodeEdges(const RelateNode* node);
+
+    NodeSections* getNodeSections(const CoordinateXY& nodePt);
+
+
+
+public:
+
+        TopologyComputer(
+            TopologyPredicate& p_predicate,
+            RelateGeometry& p_geomA,
+            RelateGeometry& p_geomB)
+            : predicate(p_predicate)
+            , geomA(p_geomA)
+            , geomB(p_geomB)
+            {
+                initExteriorDims();
+            };
+
+    int getDimension(bool isA) const;
+
+    bool isAreaArea() const;
+
+    /**
+     * Indicates whether the input geometries require self-noding
+     * for correct evaluation of specific spatial predicates.
+     * Self-noding is required for geometries which may self-cross
+     * - i.e. lines, and overlapping polygons in GeometryCollections.
+     * Self-noding is not required for polygonal geometries.
+     * This ensures that the locations of nodes created by
+     * crossing segments are computed explicitly.
+     * This ensures that node locations match in situations
+     * where a self-crossing and mutual crossing occur at the same logical location.
+     * E.g. a self-crossing line tested against a single segment
+     * identical to one of the crossed segments.
+     *
+     * @return true if self-noding is required
+     */
+    bool isSelfNodingRequired() const;
+
+    bool isExteriorCheckRequired(bool isA) const;
+
+    bool isResultKnown() const;
+
+    bool getResult() const;
+
+    /**
+     * Finalize the evaluation.
+     */
+    void finish();
+
+    void addIntersection(NodeSection* a, NodeSection* b);
+
+    void addPointOnPointInterior(const CoordinateXY* pt);
+
+    void addPointOnPointExterior(bool isGeomA, const CoordinateXY* pt);
+
+    void addPointOnGeometry(bool isA, Location locTarget, int dimTarget, const CoordinateXY* pt);
+
+    void addLineEndOnGeometry(bool isLineA, Location locLineEnd, Location locTarget, int dimTarget, const CoordinateXY* pt);
+
+    /**
+     * Adds topology for an area vertex interaction with a target geometry element.
+     * Assumes the target geometry element has highest dimension
+     * (i.e. if the point lies on two elements of different dimension,
+     * the location on the higher dimension element is provided.
+     * This is the semantic provided by {@link RelatePointLocator}.
+     *
+     * Note that in a GeometryCollection containing overlapping or adjacent polygons,
+     * the area vertex location may be INTERIOR instead of BOUNDARY.
+     *
+     * @param isAreaA the input that is the area
+     * @param locArea the location on the area
+     * @param locTarget the location on the target geometry element
+     * @param dimTarget the dimension of the target geometry element
+     * @param pt the point of interaction
+     */
+    void addAreaVertex(bool isAreaA, Location locArea, Location locTarget, int dimTarget, const CoordinateXY* pt);
+
+    void addAreaVertexOnArea(bool isAreaA, Location locArea, Location locTarget, const CoordinateXY* pt);
+
+    void evaluateNodes();
+
+    /**
+     * Disable copy construction and assignment. Apparently needed to make this
+     * class compile under MSVC. (See https://stackoverflow.com/q/29565299)
+     */
+    TopologyComputer(const TopologyComputer&) = delete;
+    TopologyComputer& operator=(const TopologyComputer&) = delete;
+
+
+};
+
+} // namespace geos.operation.relateng
+} // namespace geos.operation
+} // namespace geos
+
diff --git a/include/geos/operation/relateng/TopologyPredicate.h b/include/geos/operation/relateng/TopologyPredicate.h
new file mode 100644
index 000000000..6b594dbca
--- /dev/null
+++ b/include/geos/operation/relateng/TopologyPredicate.h
@@ -0,0 +1,206 @@
+/**********************************************************************
+ *
+ * GEOS - Geometry Engine Open Source
+ * http://geos.osgeo.org
+ *
+ * Copyright (c) 2024 Martin Davis
+ * Copyright (C) 2024 Paul Ramsey <pramsey at cleverelephant.ca>
+ *
+ * This is free software; you can redistribute and/or modify it under
+ * the terms of the GNU Lesser General Public Licence as published
+ * by the Free Software Foundation.
+ * See the COPYING file for more information.
+ *
+ **********************************************************************/
+
+#pragma once
+
+#include <geos/geom/Location.h>
+#include <geos/export.h>
+
+
+// Forward declarations
+namespace geos {
+namespace geom {
+    class Envelope;
+}
+}
+
+
+using geos::geom::Envelope;
+using geos::geom::Location;
+
+
+namespace geos {      // geos.
+namespace operation { // geos.operation
+namespace relateng {  // geos.operation.relateng
+
+
+class GEOS_DLL TopologyPredicate {
+
+public:
+
+    /* Virtual destructor to ensure proper cleanup of derived classes */
+    virtual ~TopologyPredicate() {};
+
+    /**
+     * Gets the name of the predicate.
+     *
+     * @return the predicate name
+     */
+    virtual std::string name() const = 0;
+
+    /**
+     * Indicates that the value of the predicate can be finalized
+     * based on its current state.
+     */
+    virtual void finish() = 0;
+
+    /**
+     * Tests if the predicate value is known.
+     *
+     * @return true if the result is known
+     */
+    virtual bool isKnown() const = 0;
+
+    /**
+     * Gets the current value of the predicate result.
+     * The value is only valid if isKnown() is true.
+     *
+     * @return the predicate result value
+     */
+    virtual bool value() const = 0;
+
+    /**
+     * Reports whether this predicate requires self-noding for
+     * geometries which contain crossing edges
+     * (for example, LineString, or GeometryCollection
+     * containing lines or polygons which may self-intersect).
+     * Self-noding ensures that intersections are computed consistently
+     * in cases which contain self-crossings and mutual crossings.
+     *
+     * Most predicates require this, but it can
+     * be avoided for simple intersection detection
+     * (such as in RelatePredicate#intersects()
+     * and RelatePredicate#disjoint().
+     * Avoiding self-noding improves performance for polygonal inputs.
+     *
+     * @return true if self-noding is required.
+     */
+    virtual bool requireSelfNoding() const {
+        return true;
+    };
+
+    /**
+     * Reports whether this predicate requires interaction between
+     * the input geometries.
+     * This is the case if
+     *
+     * IM[I, I] >= 0 or IM[I, B] >= 0 or IM[B, I] >= 0 or IM[B, B] >= 0
+     *
+     * This allows a fast result if
+     * the envelopes of the geometries are disjoint.
+     *
+     * @return true if the geometries must interact
+     */
+    virtual bool requireInteraction() const {
+        return true;
+    };
+
+    /**
+     * Reports whether this predicate requires that the source
+     * cover the target.
+     * This is the case if
+     *
+     * IM[Ext(Src), Int(Tgt)] = F and IM[Ext(Src), Bdy(Tgt)] = F
+     *
+     * If true, this allows a fast result if
+     * the source envelope does not cover the target envelope.
+     *
+     * @param isSourceA indicates the source input geometry
+     * @return true if the predicate requires checking whether the source covers the target
+     */
+    virtual bool requireCovers(bool isSourceA) {
+        (void)isSourceA;
+        return false;
+    }
+
+    /**
+     * Reports whether this predicate requires checking if the source input intersects
+     * the Exterior of the target input.
+     * This is the case if:
+     *
+     * IM[Int(Src), Ext(Tgt)] >= 0 or IM[Bdy(Src), Ext(Tgt)] >= 0
+     *
+     * If false, this may permit a faster result in some geometric situations.
+     *
+     * @param isSourceA indicates the source input geometry
+     * @return true if the predicate requires checking whether the source intersects the target exterior
+     */
+    virtual bool requireExteriorCheck(bool isSourceA) const {
+        (void)isSourceA;
+        return true;
+    }
+
+    /**
+     * Initializes the predicate for a specific geometric case.
+     * This may allow the predicate result to become known
+     * if it can be inferred from the dimensions.
+     *
+     * @param dimA the dimension of geometry A
+     * @param dimB the dimension of geometry B
+     *
+     * @see Dimension
+     */
+    virtual void init(int dimA, int dimB) {
+        (void)dimA;
+        (void)dimB;
+    };
+
+    /**
+     * Initializes the predicate for a specific geometric case.
+     * This may allow the predicate result to become known
+     * if it can be inferred from the envelopes.
+     *
+     * @param envA the envelope of geometry A
+     * @param envB the envelope of geometry B
+     */
+    virtual void init(const Envelope& envA, const Envelope& envB)
+    {
+        //-- default if envelopes provide no information
+        (void)envA;
+        (void)envB;
+    };
+
+    /**
+     * Updates the entry in the DE-9IM intersection matrix
+     * for given Location in the input geometries.
+     *
+     * If this method is called with a {@link Dimension} value
+     * which is less than the current value for the matrix entry,
+     * the implementing class should avoid changing the entry
+     * if this would cause information loss.
+     *
+     * @param locA the location on the A axis of the matrix
+     * @param locB the location on the B axis of the matrix
+     * @param dimension the dimension value for the entry
+     *
+     * @see Dimension
+     * @see Location
+     */
+    virtual void updateDimension(Location locA, Location locB, int dimension) = 0;
+
+
+    friend std::ostream&
+    operator<<(std::ostream& os, const TopologyPredicate& ns)
+    {
+        os << ns.name();
+        return os;
+    }
+
+};
+
+} // namespace geos.operation.relateng
+} // namespace geos.operation
+} // namespace geos
+
diff --git a/include/geos/operation/valid/IsSimpleOp.h b/include/geos/operation/valid/IsSimpleOp.h
index b26d6d53f..1f2abfa68 100644
--- a/include/geos/operation/valid/IsSimpleOp.h
+++ b/include/geos/operation/valid/IsSimpleOp.h
@@ -29,9 +29,9 @@ namespace noding {
 class SegmentString;
 class BasicSegmentString;
 }
-namespace algorithm {
-class BoundaryNodeRule;
-}
+// namespace algorithm {
+// class BoundaryNodeRule;
+// }
 namespace geom {
 class LineString;
 class LinearRing;
diff --git a/src/algorithm/PolygonNodeTopology.cpp b/src/algorithm/PolygonNodeTopology.cpp
index 588cbdf9f..8449944dd 100644
--- a/src/algorithm/PolygonNodeTopology.cpp
+++ b/src/algorithm/PolygonNodeTopology.cpp
@@ -24,6 +24,7 @@ using geos::geom::Quadrant;
 namespace geos {
 namespace algorithm { // geos.algorithm
 
+
 /* public static */
 bool
 PolygonNodeTopology::isCrossing(const CoordinateXY* nodePt,
@@ -36,16 +37,24 @@ PolygonNodeTopology::isCrossing(const CoordinateXY* nodePt,
         aLo = a1;
         aHi = a0;
     }
+
+    // bool bBetween0 = isBetween(nodePt, b0, aLo, aHi);
+    // bool bBetween1 = isBetween(nodePt, b1, aLo, aHi);
+    // return bBetween0 != bBetween1;
+
     /**
      * Find positions of b0 and b1.
      * If they are the same they do not cross the other edge
      */
-    bool bBetween0 = isBetween(nodePt, b0, aLo, aHi);
-    bool bBetween1 = isBetween(nodePt, b1, aLo, aHi);
+    int compBetween0 = compareBetween(nodePt, b0, aLo, aHi);
+    if (compBetween0 == 0) return false;
+    int compBetween1 = compareBetween(nodePt, b1, aLo, aHi);
+    if (compBetween1 == 0) return false;
 
-    return bBetween0 != bBetween1;
+    return compBetween0 != compBetween1;
 }
 
+
 /* public static */
 bool
 PolygonNodeTopology::isInteriorSegment(const CoordinateXY* nodePt,
@@ -78,6 +87,52 @@ PolygonNodeTopology::isBetween(const CoordinateXY* origin,
 }
 
 
+/* private static */
+int
+PolygonNodeTopology::compareBetween(
+    const CoordinateXY* origin, const CoordinateXY* p,
+    const CoordinateXY* e0, const CoordinateXY* e1)
+{
+    int comp0 = compareAngle(origin, p, e0);
+    if (comp0 == 0) return 0;
+    int comp1 = compareAngle(origin, p, e1);
+    if (comp1 == 0) return 0;
+    if (comp0 > 0 && comp1 < 0) return 1;
+    return -1;
+}
+
+
+/* public static */
+int
+PolygonNodeTopology::compareAngle(
+    const CoordinateXY* origin,
+    const CoordinateXY* p,
+    const CoordinateXY* q)
+{
+    int quadrantP = quadrant(origin, p);
+    int quadrantQ = quadrant(origin, q);
+
+    /**
+     * If the vectors are in different quadrants,
+     * that determines the ordering
+     */
+    if (quadrantP > quadrantQ) return 1;
+    if (quadrantP < quadrantQ) return -1;
+
+    //--- vectors are in the same quadrant
+    // Check relative orientation of vectors
+    // P > Q if it is CCW of Q
+    int orient = Orientation::index(*origin, *q, *p);
+    switch (orient) {
+        case Orientation::COUNTERCLOCKWISE:
+            return 1;
+        case Orientation::CLOCKWISE:
+            return -1;
+        default:
+            return 0;
+    }
+}
+
 /* private static */
 bool
 PolygonNodeTopology::isAngleGreater(const CoordinateXY* origin,
diff --git a/src/geom/Geometry.cpp b/src/geom/Geometry.cpp
index 8a18f2f93..7d5f17c6f 100644
--- a/src/geom/Geometry.cpp
+++ b/src/geom/Geometry.cpp
@@ -45,6 +45,8 @@
 #include <geos/operation/predicate/RectangleContains.h>
 #include <geos/operation/predicate/RectangleIntersects.h>
 #include <geos/operation/relate/RelateOp.h>
+#include <geos/operation/relateng/RelateNG.h>
+#include <geos/operation/relateng/RelatePredicate.h>
 #include <geos/operation/valid/IsValidOp.h>
 #include <geos/operation/union/UnaryUnionOp.h>
 #include <geos/operation/buffer/BufferOp.h>
@@ -71,6 +73,7 @@
 
 #define SHORTCIRCUIT_PREDICATES 1
 //#define USE_RECTANGLE_INTERSECTION 1
+#define USE_RELATENG 1
 
 using namespace geos::algorithm;
 using namespace geos::operation::valid;
@@ -257,7 +260,11 @@ Geometry::getEnvelope() const
 bool
 Geometry::disjoint(const Geometry* g) const
 {
+#if USE_RELATENG
+    return operation::relateng::RelateNG::disjoint(this, g);
+#else
     return !intersects(g);
+#endif
 }
 
 bool
@@ -269,9 +276,14 @@ Geometry::touches(const Geometry* g) const
         return false;
     }
 #endif
+
+#if USE_RELATENG
+    return operation::relateng::RelateNG::touches(this, g);
+#else
     std::unique_ptr<IntersectionMatrix> im(relate(g));
     bool res = im->isTouches(getDimension(), g->getDimension());
     return res;
+#endif
 }
 
 bool
@@ -310,6 +322,9 @@ Geometry::intersects(const Geometry* g) const
         return predicate::RectangleIntersects::intersects(*p, *this);
     }
 
+#if USE_RELATENG
+    return operation::relateng::RelateNG::intersects(this, g);
+#else
     if (getGeometryTypeId() == GEOS_GEOMETRYCOLLECTION) {
         auto im = relate(g);
         bool res = im->isIntersects();
@@ -317,12 +332,16 @@ Geometry::intersects(const Geometry* g) const
     } else {
         return prep::PreparedGeometryFactory::prepare(this)->intersects(g);
     }
+#endif
 }
 
 /*public*/
 bool
 Geometry::covers(const Geometry* g) const
 {
+#if USE_RELATENG
+    return operation::relateng::RelateNG::covers(this, g);
+#else
     // optimization - lower dimension cannot cover areas
     if(g->getDimension() == 2 && getDimension() < 2) {
         return false;
@@ -350,12 +369,27 @@ Geometry::covers(const Geometry* g) const
 
     std::unique_ptr<IntersectionMatrix> im(relate(g));
     return im->isCovers();
+#endif
 }
 
+/*public*/
+bool
+Geometry::coveredBy(const Geometry* g) const
+{
+#if USE_RELATENG
+    return operation::relateng::RelateNG::coveredBy(this, g);
+#else
+    return covers(g, this);
+#endif
+}
 
 bool
 Geometry::crosses(const Geometry* g) const
 {
+#if USE_RELATENG
+    return operation::relateng::RelateNG::crosses(this, g);
+#else
+
 #ifdef SHORTCIRCUIT_PREDICATES
     // short-circuit test
     if(! getEnvelopeInternal()->intersects(g->getEnvelopeInternal())) {
@@ -365,17 +399,27 @@ Geometry::crosses(const Geometry* g) const
     std::unique_ptr<IntersectionMatrix> im(relate(g));
     bool res = im->isCrosses(getDimension(), g->getDimension());
     return res;
+
+#endif
 }
 
 bool
 Geometry::within(const Geometry* g) const
 {
+#if USE_RELATENG
+    return operation::relateng::RelateNG::within(this, g);
+#else
     return g->contains(this);
+#endif
 }
 
 bool
 Geometry::contains(const Geometry* g) const
 {
+#if USE_RELATENG
+    return operation::relateng::RelateNG::contains(this, g);
+#else
+
     // optimization - lower dimension cannot contain areas
     if(g->getDimension() == 2 && getDimension() < 2) {
         return false;
@@ -408,11 +452,16 @@ Geometry::contains(const Geometry* g) const
     std::unique_ptr<IntersectionMatrix> im(relate(g));
     bool res = im->isContains();
     return res;
+#endif
 }
 
 bool
 Geometry::overlaps(const Geometry* g) const
 {
+#if USE_RELATENG
+    return operation::relateng::RelateNG::overlaps(this, g);
+#else
+
 #ifdef SHORTCIRCUIT_PREDICATES
     // short-circuit test
     if(! getEnvelopeInternal()->intersects(g->getEnvelopeInternal())) {
@@ -422,19 +471,29 @@ Geometry::overlaps(const Geometry* g) const
     std::unique_ptr<IntersectionMatrix> im(relate(g));
     bool res = im->isOverlaps(getDimension(), g->getDimension());
     return res;
+
+#endif
 }
 
 bool
 Geometry::relate(const Geometry* g, const std::string& intersectionPattern) const
 {
+#if USE_RELATENG
+    return operation::relateng::RelateNG::relate(this, g, intersectionPattern);
+#else
     std::unique_ptr<IntersectionMatrix> im(relate(g));
     bool res = im->matches(intersectionPattern);
     return res;
+#endif
 }
 
 bool
 Geometry::equals(const Geometry* g) const
 {
+#if USE_RELATENG
+    return operation::relateng::RelateNG::equalsTopo(this, g);
+#else
+
 #ifdef SHORTCIRCUIT_PREDICATES
     // short-circuit test
     if(! getEnvelopeInternal()->equals(g->getEnvelopeInternal())) {
@@ -452,12 +511,17 @@ Geometry::equals(const Geometry* g) const
     std::unique_ptr<IntersectionMatrix> im(relate(g));
     bool res = im->isEquals(getDimension(), g->getDimension());
     return res;
+#endif
 }
 
 std::unique_ptr<IntersectionMatrix>
 Geometry::relate(const Geometry* other) const
 {
+#if USE_RELATENG
+    return operation::relateng::RelateNG::relate(this, other);
+#else
     return RelateOp::relate(this, other);
+#endif
 }
 
 std::unique_ptr<IntersectionMatrix>
diff --git a/src/geom/GeometryCollection.cpp b/src/geom/GeometryCollection.cpp
index 096ca3526..80ecbccae 100644
--- a/src/geom/GeometryCollection.cpp
+++ b/src/geom/GeometryCollection.cpp
@@ -456,5 +456,7 @@ GeometryCollection::reverseImpl() const
     return getFactory()->createGeometryCollection(std::move(reversed)).release();
 }
 
+
+
 } // namespace geos::geom
 } // namespace geos
diff --git a/src/geom/prep/BasicPreparedGeometry.cpp b/src/geom/prep/BasicPreparedGeometry.cpp
index 3dc08f569..c346fcf42 100644
--- a/src/geom/prep/BasicPreparedGeometry.cpp
+++ b/src/geom/prep/BasicPreparedGeometry.cpp
@@ -19,6 +19,7 @@
 
 #include <geos/geom/prep/BasicPreparedGeometry.h>
 #include <geos/geom/Coordinate.h>
+#include <geos/geom/IntersectionMatrix.h>
 #include <geos/algorithm/PointLocator.h>
 #include <geos/geom/util/ComponentCoordinateExtracter.h>
 #include <geos/operation/distance/DistanceOp.h>
@@ -89,74 +90,79 @@ BasicPreparedGeometry::isAnyTargetComponentInTest(const geom::Geometry* testGeom
     return false;
 }
 
+bool
+BasicPreparedGeometry::within(const geom::Geometry* g) const
+{
+    return getRelateNG()->within(g);
+}
+
 bool
 BasicPreparedGeometry::contains(const geom::Geometry* g) const
 {
-    return baseGeom->contains(g);
+    return getRelateNG()->contains(g);
 }
 
 bool
 BasicPreparedGeometry::containsProperly(const geom::Geometry* g)	const
 {
-    // since raw relate is used, provide some optimizations
-
-    // short-circuit test
-    if(! baseGeom->getEnvelopeInternal()->contains(g->getEnvelopeInternal())) {
-        return false;
-    }
-
-    // otherwise, compute using relate mask
-    return baseGeom->relate(g, "T**FF*FF*");
+    return getRelateNG()->relate(g, "T**FF*FF*");
 }
 
 bool
 BasicPreparedGeometry::coveredBy(const geom::Geometry* g) const
 {
-    return baseGeom->coveredBy(g);
+    return getRelateNG()->coveredBy(g);
 }
 
 bool
 BasicPreparedGeometry::covers(const geom::Geometry* g) const
 {
-    return baseGeom->covers(g);
+    return getRelateNG()->covers(g);
 }
 
 bool
 BasicPreparedGeometry::crosses(const geom::Geometry* g) const
 {
-    return baseGeom->crosses(g);
+    return getRelateNG()->crosses(g);
 }
 
 bool
 BasicPreparedGeometry::disjoint(const geom::Geometry* g)	const
 {
-    return ! intersects(g);
+    return getRelateNG()->disjoint(g);
 }
 
 bool
 BasicPreparedGeometry::intersects(const geom::Geometry* g) const
 {
-    return baseGeom->intersects(g);
+    return getRelateNG()->intersects(g);
 }
 
 bool
 BasicPreparedGeometry::overlaps(const geom::Geometry* g)	const
 {
-    return baseGeom->overlaps(g);
+    return getRelateNG()->overlaps(g);
 }
 
 bool
 BasicPreparedGeometry::touches(const geom::Geometry* g) const
 {
-    return baseGeom->touches(g);
+    return getRelateNG()->touches(g);
 }
 
 bool
-BasicPreparedGeometry::within(const geom::Geometry* g) const
+BasicPreparedGeometry::relate(const geom::Geometry* g, const std::string& pat) const
 {
-    return baseGeom->within(g);
+    return getRelateNG()->relate(g, pat);
 }
 
+std::unique_ptr<IntersectionMatrix>
+BasicPreparedGeometry::relate(const geom::Geometry* g) const
+{
+    return getRelateNG()->relate(g);
+}
+
+
 std::unique_ptr<geom::CoordinateSequence>
 BasicPreparedGeometry::nearestPoints(const geom::Geometry* g) const
 {
diff --git a/src/io/WKTWriter.cpp b/src/io/WKTWriter.cpp
index 116db3178..53fa14269 100644
--- a/src/io/WKTWriter.cpp
+++ b/src/io/WKTWriter.cpp
@@ -107,17 +107,11 @@ WKTWriter::toLineString(const CoordinateSequence& seq)
 
 /*static*/
 std::string
-WKTWriter::toLineString(const Coordinate& p0, const Coordinate& p1)
+WKTWriter::toLineString(const CoordinateXY& p0, const CoordinateXY& p1)
 {
     std::stringstream ret(std::ios_base::in | std::ios_base::out);
     ret << "LINESTRING (" << p0.x << " " << p0.y;
-#if PRINT_Z
-    ret << " " << p0.z;
-#endif
     ret << ", " << p1.x << " " << p1.y;
-#if PRINT_Z
-    ret << " " << p1.z;
-#endif
     ret << ")";
 
     return ret.str();
diff --git a/src/operation/relateng/AdjacentEdgeLocator.cpp b/src/operation/relateng/AdjacentEdgeLocator.cpp
new file mode 100644
index 000000000..aeb5f8084
--- /dev/null
+++ b/src/operation/relateng/AdjacentEdgeLocator.cpp
@@ -0,0 +1,159 @@
+/**********************************************************************
+ *
+ * GEOS - Geometry Engine Open Source
+ * http://geos.osgeo.org
+ *
+ * Copyright (c) 2024 Martin Davis
+ * Copyright (C) 2024 Paul Ramsey <pramsey at cleverelephant.ca>
+ *
+ * This is free software; you can redistribute and/or modify it under
+ * the terms of the GNU Lesser General Public Licence as published
+ * by the Free Software Foundation.
+ * See the COPYING file for more information.
+ *
+ **********************************************************************/
+
+
+#include <geos/algorithm/Orientation.h>
+#include <geos/algorithm/PointLocation.h>
+#include <geos/geom/Coordinate.h>
+#include <geos/geom/Dimension.h>
+#include <geos/geom/Geometry.h>
+#include <geos/geom/LinearRing.h>
+#include <geos/geom/Polygon.h>
+#include <geos/operation/relateng/AdjacentEdgeLocator.h>
+#include <geos/operation/relateng/NodeSection.h>
+#include <geos/operation/relateng/NodeSections.h>
+#include <geos/operation/relateng/RelateNode.h>
+#include <geos/constants.h>
+
+
+using geos::algorithm::Orientation;
+using geos::algorithm::PointLocation;
+using geos::geom::CoordinateXY;
+using geos::geom::Dimension;
+using geos::geom::Geometry;
+using geos::geom::LinearRing;
+using geos::geom::Location;
+using geos::geom::Polygon;
+
+
+namespace geos {      // geos
+namespace operation { // geos.operation
+namespace relateng {  // geos.operation.relateng
+
+
+/* public */
+Location
+AdjacentEdgeLocator::locate(const CoordinateXY* p)
+{
+    NodeSections sections(p);
+    for (const CoordinateSequence* ring : ringList) {
+        addSections(p, ring, sections);
+    }
+    std::unique_ptr<RelateNode> node = sections.createNode();
+
+    return node->hasExteriorEdge(true) ? Location::BOUNDARY : Location::INTERIOR;
+}
+
+
+/* private */
+void
+AdjacentEdgeLocator::addSections(
+    const CoordinateXY* p,
+    const CoordinateSequence* ring,
+    NodeSections& sections)
+{
+    for (std::size_t i = 0; i < ring->getSize() - 1; i++) {
+        const CoordinateXY& p0 = ring->getAt(i);
+        const CoordinateXY& pnext = ring->getAt(i+1);
+        if (p->equals2D(pnext)) {
+            //-- segment final point is assigned to next segment
+            continue;
+        }
+        else if (p->equals2D(p0)) {
+            std::size_t iprev = i > 0 ? i - 1 : ring->getSize() - 2;
+            const CoordinateXY& pprev = ring->getAt(iprev);
+            NodeSection *ns = createSection(p, &pprev, &pnext);
+            sections.addNodeSection(ns);
+        }
+        else if (PointLocation::isOnSegment(*p, p0, pnext)) {
+            NodeSection *ns = createSection(p, &p0, &pnext);
+            sections.addNodeSection(ns);
+        }
+    }
+}
+
+
+/* private */
+NodeSection*
+AdjacentEdgeLocator::createSection(const CoordinateXY* p,
+    const CoordinateXY* prev,
+    const CoordinateXY* next)
+{
+    if (prev->distance(*p) == 0 || next->distance(*p) == 0) {
+        //System.out.println("Found zero-length section segment");
+    };
+    return new NodeSection(true, Dimension::A, 1, 0, nullptr, false, prev, *p, next);
+}
+
+
+/* private */
+void
+AdjacentEdgeLocator::init(const Geometry* geom)
+{
+    if (geom->isEmpty())
+        return;
+    addRings(geom);
+}
+
+
+/* private */
+void
+AdjacentEdgeLocator::addRings(const Geometry* geom)
+{
+    if (const Polygon* poly = dynamic_cast<const Polygon*>(geom)) {
+        const LinearRing* shell = poly->getExteriorRing();
+        addRing(shell, true);
+        for (std::size_t i = 0; i < poly->getNumInteriorRing(); i++) {
+            const LinearRing* hole = poly->getInteriorRingN(i);
+            addRing(hole, false);
+        }
+    }
+    else if (geom->isCollection()) {
+        //-- recurse through collections
+        for (std::size_t i = 0; i < geom->getNumGeometries(); i++) {
+            addRings(geom->getGeometryN(i));
+        }
+    }
+}
+
+
+/* private */
+void
+AdjacentEdgeLocator::addRing(const LinearRing* ring, bool requireCW)
+{
+    //TODO: remove repeated points?
+    const CoordinateSequence* pts = ring->getCoordinatesRO();
+    bool isFlipped = requireCW == Orientation::isCCW(pts);
+    /*
+     * In case of flipped rings, we need to keep a local copy
+     * since we cannot mutate the const geometry we are fed
+     * in the constructor.
+     */
+    if (isFlipped) {
+        std::unique_ptr<CoordinateSequence> localPts = pts->clone();
+        localPts->reverse();
+        ringList.push_back(localPts.get());
+        localRingList.push_back(std::move(localPts));
+    }
+    else {
+        ringList.push_back(pts);
+    }
+    return;
+}
+
+
+} // namespace geos.operation.overlayng
+} // namespace geos.operation
+} // namespace geos
diff --git a/src/operation/relateng/BasicPredicate.cpp b/src/operation/relateng/BasicPredicate.cpp
new file mode 100644
index 000000000..c225de6fe
--- /dev/null
+++ b/src/operation/relateng/BasicPredicate.cpp
@@ -0,0 +1,137 @@
+/**********************************************************************
+ *
+ * GEOS - Geometry Engine Open Source
+ * http://geos.osgeo.org
+ *
+ * Copyright (c) 2024 Martin Davis
+ * Copyright (C) 2024 Paul Ramsey <pramsey at cleverelephant.ca>
+ *
+ * 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/operation/relateng/BasicPredicate.h>
+#include <geos/geom/Envelope.h>
+#include <geos/geom/Location.h>
+#include <geos/constants.h>
+
+#include <sstream>
+
+
+using geos::geom::Envelope;
+using geos::geom::Location;
+
+
+namespace geos {      // geos
+namespace operation { // geos.operation
+namespace relateng {  // geos.operation.relateng
+
+
+
+/* private static */
+bool
+BasicPredicate::isKnown(int val)
+{
+     return val > UNKNOWN;
+}
+
+/* private static */
+bool
+BasicPredicate::toBoolean(int val)
+{
+    return val == TRUE;
+}
+
+/* private static */
+int
+BasicPredicate::toValue(bool val)
+{
+    return val ? TRUE : FALSE;
+}
+
+
+/* public static */
+bool
+BasicPredicate::isIntersection(Location locA, Location locB)
+{
+    //-- i.e. some location on both geometries intersects
+    return locA != Location::EXTERIOR && locB != Location::EXTERIOR;
+}
+
+
+// /* public */
+// bool isSelfNodingRequired() {
+//     return false;
+//   }
+
+
+/* public override */
+bool
+BasicPredicate::isKnown() const
+{
+    return isKnown(m_value);
+}
+
+/* public override */
+bool
+BasicPredicate::value() const
+{
+    return toBoolean(m_value);
+}
+
+
+/* protected */
+void
+BasicPredicate::setValue(bool val)
+{
+    //-- don't change already-known value
+    if (isKnown())
+        return;
+    m_value = toValue(val);
+}
+
+/* protected */
+void
+BasicPredicate::setValue(int val)
+{
+    //-- don't change already-known value
+    if (isKnown())
+        return;
+    m_value = val;
+}
+
+
+/* protected */
+void
+BasicPredicate::setValueIf(bool val, bool cond)
+{
+    if (cond)
+        setValue(val);
+}
+
+/* protected */
+void
+BasicPredicate::require(bool cond)
+{
+    if (! cond)
+        setValue(false);
+}
+
+/* protected */
+void
+BasicPredicate::requireCovers(const Envelope& a, const Envelope& b)
+{
+    require(a.covers(b));
+}
+
+
+} // namespace geos.operation.overlayng
+} // namespace geos.operation
+} // namespace geos
+
+
+
+
diff --git a/src/operation/relateng/DimensionLocation.cpp b/src/operation/relateng/DimensionLocation.cpp
new file mode 100644
index 000000000..50a3457d8
--- /dev/null
+++ b/src/operation/relateng/DimensionLocation.cpp
@@ -0,0 +1,125 @@
+/**********************************************************************
+ *
+ * GEOS - Geometry Engine Open Source
+ * http://geos.osgeo.org
+ *
+ * Copyright (c) 2024 Martin Davis
+ * Copyright (C) 2024 Paul Ramsey <pramsey at cleverelephant.ca>
+ *
+ * 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/operation/relateng/DimensionLocation.h>
+#include <geos/geom/Dimension.h>
+#include <geos/geom/Location.h>
+
+
+using geos::geom::Location;
+using geos::geom::Dimension;
+
+
+namespace geos {      // geos
+namespace operation { // geos.operation
+namespace relateng {  // geos.operation.relateng
+
+
+/* public static */
+int
+DimensionLocation::locationArea(Location loc)
+{
+    switch (loc) {
+        case Location::INTERIOR: return AREA_INTERIOR;
+        case Location::BOUNDARY: return AREA_BOUNDARY;
+        default:
+            return EXTERIOR;
+    }
+}
+
+
+/* public static */
+int
+DimensionLocation::locationLine(Location loc)
+{
+    switch (loc) {
+        case Location::INTERIOR: return LINE_INTERIOR;
+        case Location::BOUNDARY: return LINE_BOUNDARY;
+        default:
+            return EXTERIOR;
+    }
+}
+
+
+/* public static */
+int
+DimensionLocation::locationPoint(Location loc)
+{
+    switch (loc) {
+        case Location::INTERIOR: return POINT_INTERIOR;
+        default:
+            return EXTERIOR;
+    }
+}
+
+
+/* public static */
+Location
+DimensionLocation::location(int dimLoc)
+{
+    switch (dimLoc) {
+        case POINT_INTERIOR:
+        case LINE_INTERIOR:
+        case AREA_INTERIOR:
+            return Location::INTERIOR;
+        case LINE_BOUNDARY:
+        case AREA_BOUNDARY:
+            return Location::BOUNDARY;
+        default:
+            return Location::EXTERIOR;
+    }
+}
+
+
+/* public static */
+int
+DimensionLocation::dimension(int dimLoc)
+{
+    switch (dimLoc) {
+        case POINT_INTERIOR:
+            return Dimension::P;
+        case LINE_INTERIOR:
+        case LINE_BOUNDARY:
+            return Dimension::L;
+        case AREA_INTERIOR:
+        case AREA_BOUNDARY:
+            return Dimension::A;
+        default:
+            return Dimension::False;
+    }
+}
+
+
+/* public static */
+int
+DimensionLocation::dimension(int dimLoc, int exteriorDim)
+{
+    if (dimLoc == EXTERIOR)
+        return exteriorDim;
+    else
+        return dimension(dimLoc);
+}
+
+
+
+
+
+} // namespace geos.operation.overlayng
+} // namespace geos.operation
+} // namespace geos
+
+
+
+
diff --git a/src/operation/relateng/EdgeSegmentIntersector.cpp b/src/operation/relateng/EdgeSegmentIntersector.cpp
new file mode 100644
index 000000000..8ea3c5205
--- /dev/null
+++ b/src/operation/relateng/EdgeSegmentIntersector.cpp
@@ -0,0 +1,114 @@
+/**********************************************************************
+ *
+ * GEOS - Geometry Engine Open Source
+ * http://geos.osgeo.org
+ *
+ * Copyright (c) 2024 Martin Davis
+ * Copyright (C) 2024 Paul Ramsey <pramsey at cleverelephant.ca>
+ *
+ * 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/Geometry.h>
+#include <geos/geom/Coordinate.h>
+#include <geos/operation/relateng/EdgeSegmentIntersector.h>
+#include <geos/operation/relateng/RelateSegmentString.h>
+#include <geos/operation/relateng/TopologyComputer.h>
+#include <geos/operation/relateng/NodeSection.h>
+#include <geos/index/chain/MonotoneChain.h>
+#include <geos/index/chain/MonotoneChainBuilder.h>
+
+
+using geos::geom::CoordinateXYZM;
+using geos::geom::CoordinateXY;
+using geos::geom::Geometry;
+using geos::noding::SegmentString;
+using geos::index::chain::MonotoneChain;
+using geos::index::chain::MonotoneChainBuilder;
+
+
+namespace geos {      // geos
+namespace operation { // geos.operation
+namespace relateng {  // geos.operation.relateng
+
+
+/* public override */
+bool
+EdgeSegmentIntersector::isDone() const
+{
+    return topoComputer.isResultKnown();
+}
+
+
+/* public override */
+void
+EdgeSegmentIntersector::processIntersections(
+    SegmentString* ss0, std::size_t segIndex0,
+    SegmentString* ss1, std::size_t segIndex1)
+{
+    // don't intersect a segment with itself
+    if (ss0 == ss1 && segIndex0 == segIndex1)
+        return;
+
+    RelateSegmentString* rss0 = static_cast<RelateSegmentString*>(ss0);
+    RelateSegmentString* rss1 = static_cast<RelateSegmentString*>(ss1);
+    //TODO: move this ordering logic to TopologyBuilder
+    if (rss0->isA()) {
+        addIntersections(rss0, segIndex0, rss1, segIndex1);
+    }
+    else {
+        addIntersections(rss1, segIndex1, rss0, segIndex0);
+    }
+}
+
+
+/* private */
+void
+EdgeSegmentIntersector::addIntersections(
+    RelateSegmentString* ssA, std::size_t segIndexA,
+    RelateSegmentString* ssB, std::size_t segIndexB)
+{
+
+    const CoordinateXY& a0 = ssA->getCoordinate(segIndexA);
+    const CoordinateXY& a1 = ssA->getCoordinate(segIndexA + 1);
+    const CoordinateXY& b0 = ssB->getCoordinate(segIndexB);
+    const CoordinateXY& b1 = ssB->getCoordinate(segIndexB + 1);
+
+    li.computeIntersection(a0, a1, b0, b1);
+
+    if (! li.hasIntersection())
+        return;
+
+    for (uint32_t i = 0; i < li.getIntersectionNum(); i++)
+    {
+        const CoordinateXYZM& intPtXYZM = li.getIntersection(i);
+        CoordinateXY intPt(intPtXYZM.x, intPtXYZM.y);
+        /**
+         * Ensure endpoint intersections are added once only, for their canonical segments.
+         * Proper intersections lie on a unique segment so do not need to be checked.
+         * And it is important that the Containing Segment check not be used,
+         * since due to intersection computation roundoff,
+         * it is not reliable in that situation.
+         */
+        if (li.isProper() ||
+            (ssA->isContainingSegment(segIndexA, &intPt) &&
+             ssB->isContainingSegment(segIndexB, &intPt)))
+        {
+            NodeSection* nsa = ssA->createNodeSection(segIndexA, intPt);
+            NodeSection* nsb = ssB->createNodeSection(segIndexB, intPt);
+            topoComputer.addIntersection(nsa, nsb);
+        }
+    }
+}
+
+
+
+} // namespace geos.operation.overlayng
+} // namespace geos.operation
+} // namespace geos
+
+
diff --git a/src/operation/relateng/EdgeSegmentOverlapAction.cpp b/src/operation/relateng/EdgeSegmentOverlapAction.cpp
new file mode 100644
index 000000000..cd32bca78
--- /dev/null
+++ b/src/operation/relateng/EdgeSegmentOverlapAction.cpp
@@ -0,0 +1,53 @@
+/**********************************************************************
+ *
+ * GEOS - Geometry Engine Open Source
+ * http://geos.osgeo.org
+ *
+ * Copyright (C) 2001-2002 Vivid Solutions Inc.
+ *
+ * This is free software; you can redistribute and/or modify it under
+ * the terms of the GNU Lesser General Public Licence as published
+ * by the Free Software Foundation.
+ * See the COPYING file for more information.
+ *
+ **********************************************************************
+ *
+ * Last port: index/chain/MonotoneChainOverlapAction.java rev. 1.6 (JTS-1.10)
+ *
+ **********************************************************************/
+
+#include <geos/index/chain/MonotoneChainOverlapAction.h>
+#include <geos/index/chain/MonotoneChain.h>
+#include <geos/noding/SegmentString.h>
+#include <geos/noding/SegmentIntersector.h>
+#include <geos/operation/relateng/EdgeSegmentOverlapAction.h>
+#include <geos/geom/Envelope.h>
+#include <geos/geom/LineSegment.h>
+#include <geos/profiler.h>
+
+//#include <stdio.h>
+
+using geos::index::chain::MonotoneChain;
+using geos::noding::SegmentString;
+using geos::noding::SegmentIntersector;
+
+namespace geos {
+namespace operation { // geos.operation
+namespace relateng {  // geos.operation.relateng
+
+
+/* public override */
+void
+EdgeSegmentOverlapAction::overlap(
+    const MonotoneChain& mc1, std::size_t start1,
+    const MonotoneChain& mc2, std::size_t start2)
+{
+    SegmentString* ss1 = static_cast<SegmentString*>(mc1.getContext());
+    SegmentString* ss2 = static_cast<SegmentString*>(mc2.getContext());
+    si.processIntersections(ss1, start1, ss2, start2);
+}
+
+
+} // namespace geos.index.chain
+} // namespace geos.index
+} // namespace geos
diff --git a/src/operation/relateng/EdgeSetIntersector.cpp b/src/operation/relateng/EdgeSetIntersector.cpp
new file mode 100644
index 000000000..37b3600b8
--- /dev/null
+++ b/src/operation/relateng/EdgeSetIntersector.cpp
@@ -0,0 +1,99 @@
+/**********************************************************************
+ *
+ * GEOS - Geometry Engine Open Source
+ * http://geos.osgeo.org
+ *
+ * Copyright (c) 2024 Martin Davis
+ * Copyright (C) 2024 Paul Ramsey <pramsey at cleverelephant.ca>
+ *
+ * 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/Envelope.h>
+#include <geos/geom/Geometry.h>
+#include <geos/noding/SegmentString.h>
+#include <geos/operation/relateng/EdgeSegmentIntersector.h>
+#include <geos/operation/relateng/EdgeSetIntersector.h>
+#include <geos/operation/relateng/EdgeSegmentOverlapAction.h>
+#include <geos/operation/relateng/RelateSegmentString.h>
+#include <geos/index/chain/MonotoneChain.h>
+#include <geos/index/chain/MonotoneChainBuilder.h>
+
+
+using geos::geom::Geometry;
+using geos::geom::Envelope;
+using geos::noding::SegmentString;
+using geos::index::chain::MonotoneChain;
+using geos::index::chain::MonotoneChainBuilder;
+
+
+namespace geos {      // geos
+namespace operation { // geos.operation
+namespace relateng {  // geos.operation.relateng
+
+
+/* private */
+void
+EdgeSetIntersector::addEdges(std::vector<const SegmentString*>& segStrings)
+{
+    for (const SegmentString* ss : segStrings) {
+        addToIndex(ss);
+    }
+}
+
+/* private */
+void
+EdgeSetIntersector::addToIndex(const SegmentString* segStr)
+{
+    std::vector<MonotoneChain> segChains;
+    MonotoneChainBuilder::getChains(segStr->getCoordinates(), const_cast<SegmentString*>(segStr), segChains);
+
+    for (MonotoneChain& mc : segChains) {
+        if (envelope == nullptr || envelope->intersects(mc.getEnvelope())) {
+            mc.setId(idCounter++);
+            monoChains.push_back(mc);
+            MonotoneChain* mcPtr = &(monoChains.back());
+            index.insert(mcPtr->getEnvelope(), mcPtr);
+        }
+    }
+}
+
+/* public */
+void
+EdgeSetIntersector::process(EdgeSegmentIntersector& intersector)
+{
+    EdgeSegmentOverlapAction overlapAction(intersector);
+
+    for (const MonotoneChain& queryChain : monoChains) {
+
+        std::vector<const MonotoneChain*> overlapChains;
+        index.query(queryChain.getEnvelope(), [&overlapChains](const MonotoneChain* mc) {
+            overlapChains.push_back(mc);
+            });
+
+        for (const MonotoneChain* testChain : overlapChains) {
+            /**
+             * following test makes sure we only compare each pair of chains once
+             * and that we don't compare a chain to itself
+             */
+            if (testChain->getId() <= queryChain.getId())
+                continue;
+
+            testChain->computeOverlaps(&queryChain, &overlapAction);
+            if (intersector.isDone())
+                return;
+        }
+    }
+}
+
+
+
+} // namespace geos.operation.overlayng
+} // namespace geos.operation
+} // namespace geos
+
+
diff --git a/src/operation/relateng/IMPatternMatcher.cpp b/src/operation/relateng/IMPatternMatcher.cpp
new file mode 100644
index 000000000..d70c88044
--- /dev/null
+++ b/src/operation/relateng/IMPatternMatcher.cpp
@@ -0,0 +1,149 @@
+/**********************************************************************
+ *
+ * GEOS - Geometry Engine Open Source
+ * http://geos.osgeo.org
+ *
+ * Copyright (c) 2024 Martin Davis
+ * Copyright (C) 2024 Paul Ramsey <pramsey at cleverelephant.ca>
+ *
+ * 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/operation/relateng/IMPatternMatcher.h>
+#include <geos/geom/Envelope.h>
+#include <geos/geom/Location.h>
+
+#include <sstream>
+
+
+using geos::geom::Envelope;
+using geos::geom::Location;
+
+
+namespace geos {      // geos
+namespace operation { // geos.operation
+namespace relateng {  // geos.operation.relateng
+
+
+/* public */
+std::string
+IMPatternMatcher::name() const
+{
+    return "IMPattern";
+}
+
+
+/* public */
+void
+IMPatternMatcher::init(const Envelope& envA, const Envelope& envB)
+{
+    IMPredicate::init(dimA, dimB);
+    //-- if pattern specifies any non-E/non-E interaction, envelopes must not be disjoint
+    bool requiresInteraction = requireInteraction(patternMatrix);
+    bool isDisjoint = envA.disjoint(&envB);
+    setValueIf(false, requiresInteraction && isDisjoint);
+}
+
+
+/* public */
+bool
+IMPatternMatcher::requireInteraction() const
+{
+    return requireInteraction(patternMatrix);
+}
+
+
+/* private static */
+bool
+IMPatternMatcher::requireInteraction(const IntersectionMatrix& im)
+{
+    bool requiresInteraction =
+        isInteraction(im.get(Location::INTERIOR, Location::INTERIOR)) ||
+        isInteraction(im.get(Location::INTERIOR, Location::BOUNDARY)) ||
+        isInteraction(im.get(Location::BOUNDARY, Location::INTERIOR)) ||
+        isInteraction(im.get(Location::BOUNDARY, Location::BOUNDARY));
+    return requiresInteraction;
+}
+
+
+/* private static */
+bool
+IMPatternMatcher::isInteraction(int imDim)
+{
+    return imDim == Dimension::True || imDim >= Dimension::P;
+}
+
+
+/* public */
+bool
+IMPatternMatcher::isDetermined() const
+{
+    /**
+     * Matrix entries only increase in dimension as topology is computed.
+     * The predicate can be short-circuited (as false) if
+     * any computed entry is greater than the mask value.
+     */
+    std::array<Location,3> locs = {
+        Location::INTERIOR, Location::BOUNDARY, Location::EXTERIOR};
+
+    for (Location i : locs) {
+        for (Location j : locs) {
+            int patternEntry = patternMatrix.get(i, j);
+
+            if (patternEntry == Dimension::DONTCARE)
+                continue;
+
+            int matrixVal = getDimension(i, j);
+
+            //-- mask entry TRUE requires a known matrix entry
+            if (patternEntry == Dimension::True) {
+                if (matrixVal < 0)
+                    return false;
+            }
+            //-- result is known (false) if matrix entry has exceeded mask
+            else if (matrixVal > patternEntry)
+                return true;
+        }
+    }
+    return false;
+}
+
+
+/* public */
+bool
+IMPatternMatcher::valueIM()
+{
+    bool val = intMatrix.matches(imPattern);
+    return val;
+}
+
+
+/* public */
+std::string
+IMPatternMatcher::toString() const
+{
+    return name() + "(" + imPattern + ")";
+}
+
+
+/* public friend */
+std::ostream&
+operator<<(std::ostream& os, const IMPatternMatcher& imp)
+{
+    os << imp.toString();
+    return os;
+}
+
+
+
+} // namespace geos.operation.overlayng
+} // namespace geos.operation
+} // namespace geos
+
+
+
+
diff --git a/src/operation/relateng/IMPredicate.cpp b/src/operation/relateng/IMPredicate.cpp
new file mode 100644
index 000000000..3b4411d73
--- /dev/null
+++ b/src/operation/relateng/IMPredicate.cpp
@@ -0,0 +1,155 @@
+/**********************************************************************
+ *
+ * GEOS - Geometry Engine Open Source
+ * http://geos.osgeo.org
+ *
+ * Copyright (c) 2024 Martin Davis
+ * Copyright (C) 2024 Paul Ramsey <pramsey at cleverelephant.ca>
+ *
+ * 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/operation/relateng/IMPredicate.h>
+#include <geos/geom/Envelope.h>
+#include <geos/geom/Location.h>
+#include <geos/constants.h>
+
+#include <sstream>
+
+
+using geos::geom::Envelope;
+using geos::geom::Location;
+
+
+namespace geos {      // geos
+namespace operation { // geos.operation
+namespace relateng {  // geos.operation.relateng
+
+
+/* public static */
+bool
+IMPredicate::isDimsCompatibleWithCovers(int dim0, int dim1)
+{
+    //- allow Points coveredBy zero-length Lines
+    if (dim0 == Dimension::P && dim1 == Dimension::L)
+        return true;
+    return dim0 >= dim1;
+}
+
+
+/* public */
+void
+IMPredicate::init(int dA, int dB)
+{
+    dimA = dA;
+    dimB = dB;
+}
+
+
+/* public */
+void
+IMPredicate::updateDimension(Location locA, Location locB, int dimension)
+{
+    //-- only record an increased dimension value
+    if (isDimChanged(locA, locB, dimension)) {
+        intMatrix.set(locA, locB, dimension);
+        //-- set value if predicate value can be known
+        if (isDetermined()) {
+            setValue(valueIM());
+        }
+    }
+}
+
+
+/* public */
+bool
+IMPredicate::isDimChanged(Location locA, Location locB, int dimension) const
+{
+    return dimension > intMatrix.get(locA, locB);
+}
+
+
+/* protected */
+bool
+IMPredicate::intersectsExteriorOf(bool isA) const
+{
+    if (isA) {
+        return isIntersects(Location::EXTERIOR, Location::INTERIOR)
+            || isIntersects(Location::EXTERIOR, Location::BOUNDARY);
+    }
+    else {
+        return isIntersects(Location::INTERIOR, Location::EXTERIOR)
+            || isIntersects(Location::BOUNDARY, Location::EXTERIOR);
+    }
+}
+
+
+/* protected */
+bool
+IMPredicate::isIntersects(Location locA, Location locB) const
+{
+    return intMatrix.get(locA, locB) >= Dimension::P;
+}
+
+
+/* public */
+bool
+IMPredicate::isKnown(Location locA, Location locB) const
+{
+    return intMatrix.get(locA, locB) != DIM_UNKNOWN;
+}
+
+
+/* public */
+bool
+IMPredicate::isDimension(Location locA, Location locB, int dimension) const
+{
+    return intMatrix.get(locA, locB) == dimension;
+}
+
+
+/* public */
+int
+IMPredicate::getDimension(Location locA, Location locB) const
+{
+    return intMatrix.get(locA, locB);
+}
+
+
+/* public */
+void
+IMPredicate::finish()
+{
+    setValue(valueIM());
+}
+
+
+/* public */
+std::string
+IMPredicate::toString() const
+{
+    return name() + ": " + intMatrix.toString();
+}
+
+
+/* public friend */
+std::ostream&
+operator<<(std::ostream& os, const IMPredicate& imp)
+{
+    os << imp.toString() << " " << imp.intMatrix;
+    return os;
+}
+
+
+
+} // namespace geos.operation.overlayng
+} // namespace geos.operation
+} // namespace geos
+
+
+
+
diff --git a/src/operation/relateng/LineStringExtracter.cpp b/src/operation/relateng/LineStringExtracter.cpp
new file mode 100644
index 000000000..b1c229cba
--- /dev/null
+++ b/src/operation/relateng/LineStringExtracter.cpp
@@ -0,0 +1,87 @@
+/**********************************************************************
+ *
+ * GEOS - Geometry Engine Open Source
+ * http://geos.osgeo.org
+ *
+ * Copyright (c) 2024 Martin Davis
+ * Copyright (C) 2024 Paul Ramsey <pramsey at cleverelephant.ca>
+ *
+ * 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/operation/relateng/LineStringExtracter.h>
+#include <geos/geom/Geometry.h>
+#include <geos/geom/GeometryFactory.h>
+#include <geos/geom/LineString.h>
+#include <geos/constants.h>
+#include <sstream>
+
+
+using geos::geom::Geometry;
+using geos::geom::GeometryFactory;
+using geos::geom::LineString;
+
+
+namespace geos {      // geos
+namespace operation { // geos.operation
+namespace relateng {  // geos.operation.relateng
+
+
+/* public static */
+void
+LineStringExtracter::getLines(const Geometry* geom, std::vector<const LineString*>& lines)
+{
+    if (geom->getGeometryTypeId() == geom::GEOS_LINESTRING) {
+        lines.push_back(static_cast<const LineString*>(geom));
+    }
+    else if (geom->isCollection()) {
+        LineStringExtracter lse(lines);
+        geom->apply_ro(&lse);
+    }
+    // skip non-LineString elemental geometries
+
+    return;
+}
+
+
+/* public static */
+std::vector<const LineString*>
+LineStringExtracter::getLines(const Geometry* geom)
+{
+    std::vector<const LineString*> lines;
+    getLines(geom, lines);
+    return lines;
+}
+
+
+/* public static */
+// std::unique_ptr<Geometry>
+// LineStringExtracter::getGeometry(const Geometry* geom)
+// {
+//     std::vector<const LineString*> lines;
+//     getLines(geom, lines);
+//     return geom->getFactory()->buildGeometry(lines);
+// }
+
+
+/* public */
+void
+LineStringExtracter::filter_ro(const Geometry* geom)
+{
+    if (geom->getGeometryTypeId() == geom::GEOS_LINESTRING)
+        comps.push_back(static_cast<const LineString*>(geom));
+}
+
+
+
+} // namespace geos.operation.overlayng
+} // namespace geos.operation
+} // namespace geos
+
+
+
+
diff --git a/src/operation/relateng/LinearBoundary.cpp b/src/operation/relateng/LinearBoundary.cpp
new file mode 100644
index 000000000..c97d6b84b
--- /dev/null
+++ b/src/operation/relateng/LinearBoundary.cpp
@@ -0,0 +1,114 @@
+/**********************************************************************
+ *
+ * GEOS - Geometry Engine Open Source
+ * http://geos.osgeo.org
+ *
+ * Copyright (c) 2024 Martin Davis
+ * Copyright (C) 2024 Paul Ramsey <pramsey at cleverelephant.ca>
+ *
+ * This is free software; you can redistribute and/or modify it under
+ * the terms of the GNU Lesser General Public Licence as published
+ * by the Free Software Foundation.
+ * See the COPYING file for more information.
+ *
+ **********************************************************************/
+
+#include <geos/algorithm/BoundaryNodeRule.h>
+#include <geos/geom/Coordinate.h>
+#include <geos/geom/CoordinateSequence.h>
+#include <geos/geom/LineString.h>
+#include <geos/operation/relateng/LinearBoundary.h>
+#include <geos/constants.h>
+
+#include <map>
+#include <set>
+
+using geos::algorithm::BoundaryNodeRule;
+using geos::geom::CoordinateXY;
+using geos::geom::CoordinateSequence;
+using geos::geom::LineString;
+
+
+namespace geos {      // geos
+namespace operation { // geos.operation
+namespace relateng {  // geos.operation.relateng
+
+
+LinearBoundary::LinearBoundary(std::vector<const LineString*>& lines, const BoundaryNodeRule& bnRule)
+    : m_boundaryNodeRule(bnRule)
+{
+    //assert: dim(geom) == 1
+    computeBoundaryPoints(lines, m_vertexDegree);
+    m_hasBoundary = checkBoundary(m_vertexDegree);
+}
+
+/* private */
+bool
+LinearBoundary::checkBoundary(Coordinate::ConstIntMap& vertexDegree) const
+{
+    // Iterate over the map and test the values
+    for (const auto& pair : vertexDegree) {
+        int degree = pair.second;
+        if (m_boundaryNodeRule.isInBoundary(degree)) {
+            return true;
+        }
+    }
+    return false;
+}
+
+/* public */
+bool
+LinearBoundary::hasBoundary() const
+{
+    return m_hasBoundary;
+}
+
+/* public */
+bool
+LinearBoundary::isBoundary(const CoordinateXY* pt) const
+{
+    auto it = m_vertexDegree.find(pt);
+    if (it == m_vertexDegree.end())
+        return false;
+
+    int degree = it->second;
+    return m_boundaryNodeRule.isInBoundary(degree);
+}
+
+/* private static */
+void
+LinearBoundary::computeBoundaryPoints(std::vector<const LineString*>& lines, Coordinate::ConstIntMap& vertexDegree)
+{
+    for (const LineString* line : lines) {
+        if (line->isEmpty())
+            continue;
+        const CoordinateSequence* cs = line->getCoordinatesRO();
+        const Coordinate& cs0 = cs->getAt(0);
+        const Coordinate& csn = cs->getAt(line->getNumPoints() - 1);
+        addEndpoint(&cs0, vertexDegree);
+        addEndpoint(&csn, vertexDegree);
+    }
+}
+
+/* private static */
+void
+LinearBoundary::addEndpoint(const CoordinateXY *p, Coordinate::ConstIntMap& vertexDegree)
+{
+    int dim = 0;
+    auto it = vertexDegree.find(p);
+
+    if (it != vertexDegree.end()) {
+        dim = it->second;
+    }
+    vertexDegree[p] = dim + 1;
+}
+
+
+
+} // namespace geos.operation.overlayng
+} // namespace geos.operation
+} // namespace geos
+
+
+
+
diff --git a/src/operation/relateng/NodeSection.cpp b/src/operation/relateng/NodeSection.cpp
new file mode 100644
index 000000000..d8d48f544
--- /dev/null
+++ b/src/operation/relateng/NodeSection.cpp
@@ -0,0 +1,253 @@
+/**********************************************************************
+ *
+ * GEOS - Geometry Engine Open Source
+ * http://geos.osgeo.org
+ *
+ * Copyright (c) 2024 Martin Davis
+ * Copyright (C) 2024 Paul Ramsey <pramsey at cleverelephant.ca>
+ *
+ * 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/Dimension.h>
+#include <geos/geom/Coordinate.h>
+#include <geos/geom/Geometry.h>
+#include <geos/io/WKTWriter.h>
+#include <geos/operation/relateng/NodeSection.h>
+#include <sstream>
+
+
+using geos::geom::Coordinate;
+using geos::geom::CoordinateXY;
+using geos::geom::Geometry;
+using geos::geom::Dimension;
+using geos::io::WKTWriter;
+
+
+namespace geos {      // geos
+namespace operation { // geos.operation
+namespace relateng {  // geos.operation.relateng
+
+
+/* public */
+const CoordinateXY *
+NodeSection::getVertex(int i) const
+{
+    return i == 0 ? m_v0 : m_v1;
+}
+
+
+/* public */
+const CoordinateXY &
+NodeSection::nodePt() const
+{
+    return m_nodePt;
+}
+
+
+/* public */
+int
+NodeSection::dimension() const
+{
+    return m_dim;
+}
+
+
+/* public */
+int
+NodeSection::id() const
+{
+    return m_id;
+}
+
+
+/* public */
+int
+NodeSection::ringId() const
+{
+    return m_ringId;
+}
+
+
+/* public */
+const Geometry *
+NodeSection::getPolygonal() const
+{
+    return m_poly;
+}
+
+
+/* public */
+bool
+NodeSection::isShell() const
+{
+    return m_ringId == 0;
+}
+
+
+/* public */
+bool
+NodeSection::isArea() const
+{
+    return m_dim == Dimension::A;
+}
+
+
+/* public static */
+bool
+NodeSection::isAreaArea(const NodeSection& a, const NodeSection& b)
+{
+    return a.dimension() == Dimension::A && b.dimension() == Dimension::A;
+}
+
+
+/* public */
+bool
+NodeSection::isA() const
+{
+     return m_isA;
+}
+
+
+/* public */
+bool
+NodeSection::isSameGeometry(const NodeSection& ns) const
+{
+    return isA() == ns.isA();
+}
+
+
+/* public */
+bool
+NodeSection::isSamePolygon(const NodeSection& ns) const
+{
+    return isA() == ns.isA() && id() == ns.id();
+}
+
+
+/* public */
+bool
+NodeSection::isNodeAtVertex() const
+{
+    return m_isNodeAtVertex;
+}
+
+
+/* public */
+bool
+NodeSection::isProper() const
+{
+    return ! m_isNodeAtVertex;
+}
+
+
+/* public static */
+bool
+NodeSection::isProper(const NodeSection& a, const NodeSection& b)
+{
+    return a.isProper() && b.isProper();
+}
+
+
+/* public static */
+std::string
+NodeSection::edgeRep(const CoordinateXY* p0, const CoordinateXY* p1)
+{
+    if (p0 == nullptr || p1 == nullptr)
+        return "null";
+    return WKTWriter::toLineString(*p0, *p1);
+}
+
+
+/* private */
+int
+NodeSection::compare(int a, int b)
+{
+    if (a < b) return -1;
+    if (a > b) return 1;
+    return 0;
+}
+
+
+/* public */
+int
+NodeSection::compareTo(const NodeSection& o) const
+{
+    // Assert: nodePt.equals2D(o.nodePt())
+
+    // sort A before B
+    if (m_isA != o.m_isA) {
+        if (m_isA) return -1;
+        return 1;
+    }
+    //-- sort on dimensions
+    int compDim = compare(m_dim,  o.m_dim);
+    if (compDim != 0) return compDim;
+
+    //-- sort on id and ring id
+    int compId = compare(m_id, o.m_id);
+    if (compId != 0) return compId;
+
+    int compRingId = compare(m_ringId, o.m_ringId);
+    if (compRingId != 0) return compRingId;
+
+    //-- sort on edge coordinates
+    int compV0 = compareWithNull(m_v0, o.m_v0);
+    if (compV0 != 0) return compV0;
+
+    return compareWithNull(m_v1, o.m_v1);
+}
+
+
+/* private static */
+int
+NodeSection::compareWithNull(const CoordinateXY* v0, const CoordinateXY* v1)
+{
+    if (v0 == nullptr) {
+        if (v1 == nullptr)
+            return 0;
+        //-- null is lower than non-null
+        return -1;
+    }
+    // v0 is non-null
+    if (v1 == nullptr)
+        return 1;
+    return v0->compareTo(*v1);
+}
+
+
+/* public */
+std::string
+NodeSection::toString() const
+{
+    // TODO, port RelateGeometry
+    // os << RelateGeometry::name(m_isA);
+    std::stringstream ss;
+    ss << m_dim;
+    if (m_id >= 0) {
+        ss << "[" << m_id << ":" << m_ringId << "]";
+    }
+    ss << ": " << edgeRep(m_v0, &m_nodePt);
+    ss << (m_isNodeAtVertex ? "-V-" : "---");
+    ss << " " << edgeRep(&m_nodePt, m_v1);
+    return ss.str();
+}
+
+/* public friend */
+std::ostream&
+operator<<(std::ostream& os, const NodeSection& ns)
+{
+    os << ns.toString();
+    return os;
+}
+
+
+} // namespace geos.operation.overlayng
+} // namespace geos.operation
+} // namespace geos
+
+
diff --git a/src/operation/relateng/NodeSections.cpp b/src/operation/relateng/NodeSections.cpp
new file mode 100644
index 000000000..e02433d32
--- /dev/null
+++ b/src/operation/relateng/NodeSections.cpp
@@ -0,0 +1,163 @@
+/**********************************************************************
+ *
+ * GEOS - Geometry Engine Open Source
+ * http://geos.osgeo.org
+ *
+ * Copyright (c) 2024 Martin Davis
+ * Copyright (C) 2024 Paul Ramsey <pramsey at cleverelephant.ca>
+ *
+ * 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/operation/relateng/NodeSections.h>
+#include <geos/operation/relateng/RelateNode.h>
+#include <geos/operation/relateng/PolygonNodeConverter.h>
+#include <algorithm>
+
+using geos::geom::CoordinateXY;
+using geos::geom::Geometry;
+
+
+namespace geos {      // geos
+namespace operation { // geos.operation
+namespace relateng {  // geos.operation.relateng
+
+/* public */
+const CoordinateXY*
+NodeSections::getCoordinate() const
+{
+    return nodePt;
+}
+
+
+/* public */
+void
+NodeSections::addNodeSection(NodeSection* e)
+{
+    //System.out.println(e);
+    sections.emplace_back(e);
+}
+
+
+/* public */
+bool
+NodeSections::hasInteractionAB() const
+{
+    bool isA = false;
+    bool isB = false;
+    for (const std::unique_ptr<NodeSection>& ns : sections) {
+        if (ns->isA())
+            isA = true;
+        else
+            isB = true;
+
+        if (isA && isB)
+            return true;
+    }
+    return false;
+}
+
+
+/* public */
+const Geometry*
+NodeSections::getPolygonal(bool isA) const
+{
+    for (const std::unique_ptr<NodeSection>& ns : sections) {
+        if (ns->isA() == isA) {
+            const Geometry* poly = ns->getPolygonal();
+            if (poly != nullptr)
+                return poly;
+        }
+    }
+    return nullptr;
+}
+
+
+/* public */
+std::unique_ptr<RelateNode>
+NodeSections::createNode()
+{
+    prepareSections();
+
+    std::unique_ptr<RelateNode> node(new RelateNode(nodePt));
+    std::size_t i = 0;
+    while (i < sections.size()) {
+        const std::unique_ptr<NodeSection>& ns = sections[i];
+        //-- if there multiple polygon sections incident at node convert them to maximal-ring structure
+        if (ns->isArea() && hasMultiplePolygonSections(sections, i)) {
+            std::vector<const NodeSection*> polySections = collectPolygonSections(sections, i);
+            std::vector<std::unique_ptr<NodeSection>> nsConvert = PolygonNodeConverter::convert(polySections);
+            node->addEdges(nsConvert);
+            i += polySections.size();
+        }
+        else {
+            //-- the most common case is a line or a single polygon ring section
+            node->addEdges(ns.get());
+            i += 1;
+        }
+    }
+    return node;
+}
+
+
+/* private */
+void
+NodeSections::prepareSections()
+{
+    // Comparator lambda for sort support
+    auto comparator = [](
+        const std::unique_ptr<NodeSection>& a,
+        const std::unique_ptr<NodeSection>& b)
+    {
+        return a->compareTo(*b) < 0;
+    };
+
+    std::sort(sections.begin(), sections.end(), comparator);
+    //TODO: remove duplicate sections
+}
+
+
+/* private static */
+bool
+NodeSections::hasMultiplePolygonSections(
+    std::vector<std::unique_ptr<NodeSection>>& sections,
+    std::size_t i)
+{
+    //-- if last section can only be one
+    if (i >= sections.size() - 1)
+        return false;
+    //-- check if there are at least two sections for same polygon
+    std::unique_ptr<NodeSection>& ns = sections[i];
+    std::unique_ptr<NodeSection>& nsNext = sections[i + 1];
+    return ns->isSamePolygon(*nsNext);
+}
+
+
+/* private static */
+std::vector<const NodeSection*>
+NodeSections::collectPolygonSections(
+    std::vector<std::unique_ptr<NodeSection>>& sections,
+    std::size_t i)
+{
+    std::vector<const NodeSection*> polySections;
+    //-- note ids are only unique to a geometry
+    std::unique_ptr<NodeSection>& polySection = sections[i];
+    while (i < sections.size() &&
+        polySection->isSamePolygon(*(sections[i])))
+    {
+        polySections.push_back(sections[i].get());
+        i++;
+    }
+    return polySections;
+}
+
+
+} // namespace geos.operation.overlayng
+} // namespace geos.operation
+} // namespace geos
+
+
diff --git a/src/operation/relateng/PolygonNodeConverter.cpp b/src/operation/relateng/PolygonNodeConverter.cpp
new file mode 100644
index 000000000..edc582c32
--- /dev/null
+++ b/src/operation/relateng/PolygonNodeConverter.cpp
@@ -0,0 +1,193 @@
+/**********************************************************************
+ *
+ * GEOS - Geometry Engine Open Source
+ * http://geos.osgeo.org
+ *
+ * Copyright (c) 2024 Martin Davis
+ * Copyright (C) 2024 Paul Ramsey <pramsey at cleverelephant.ca>
+ *
+ * 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/operation/relateng/PolygonNodeConverter.h>
+
+#include <geos/algorithm/PolygonNodeTopology.h>
+#include <geos/geom/Coordinate.h>
+#include <geos/geom/Dimension.h>
+#include <geos/constants.h>
+
+#include <algorithm>
+
+
+using geos::algorithm::PolygonNodeTopology;
+using geos::geom::CoordinateXY;
+using geos::geom::Dimension;
+// using geos::geom::Location;
+// using geos::geom::Position;
+
+
+namespace geos {      // geos
+namespace operation { // geos.operation
+namespace relateng {  // geos.operation.relateng
+
+
+/* public static */
+std::vector<std::unique_ptr<NodeSection>>
+PolygonNodeConverter::convert(std::vector<const NodeSection*>& polySections)
+{
+    auto comparator = [](
+        const NodeSection* ns1,
+        const NodeSection* ns2)
+    {
+        int comp = PolygonNodeTopology::compareAngle(
+            &(ns1->nodePt()),
+            ns1->getVertex(0),
+            ns2->getVertex(0));
+        return comp < 0;
+    };
+
+    std::sort(polySections.begin(), polySections.end(), comparator);
+    // polySections.sort(new NodeSection.EdgeAngleComparator());
+
+    //TODO: move uniquing up to caller
+    std::vector<const NodeSection*> sections = extractUnique(polySections);
+    if (sections.size() == 1) {
+        std::vector<std::unique_ptr<NodeSection>> nss;
+        nss.emplace_back(new NodeSection(sections[0]));
+        return nss;
+    }
+
+    //-- find shell section index
+    std::size_t shellIndex = findShell(sections);
+    if (shellIndex == INDEX_UNKNOWN) {
+        return convertHoles(sections);
+    }
+    //-- at least one shell is present.  Handle multiple ones if present
+    std::vector<std::unique_ptr<NodeSection>> convertedSections;
+    std::size_t nextShellIndex = shellIndex;
+    do {
+        nextShellIndex = convertShellAndHoles(
+            sections, nextShellIndex, convertedSections);
+    } while (nextShellIndex != shellIndex);
+
+    return convertedSections;
+}
+
+
+/* private static */
+std::size_t
+PolygonNodeConverter::convertShellAndHoles(
+    std::vector<const NodeSection*>& sections,
+    std::size_t shellIndex,
+    std::vector<std::unique_ptr<NodeSection>>& convertedSections)
+{
+    const NodeSection* shellSection = sections[shellIndex];
+    const CoordinateXY* inVertex = shellSection->getVertex(0);
+    std::size_t i = next(sections, shellIndex);
+    const NodeSection* holeSection = nullptr;
+    while (! sections[i]->isShell()) {
+        holeSection = sections[i];
+        // Assert: holeSection.isShell() = false
+        const CoordinateXY* outVertex = holeSection->getVertex(1);
+        NodeSection* ns = createSection(shellSection, inVertex, outVertex);
+        convertedSections.emplace_back(ns);
+        inVertex = holeSection->getVertex(0);
+        i = next(sections, i);
+    }
+    //-- create final section for corner from last hole to shell
+    const CoordinateXY* outVertex = shellSection->getVertex(1);
+    NodeSection* ns = createSection(shellSection, inVertex, outVertex);
+    convertedSections.emplace_back(ns);
+    return i;
+}
+
+
+/* private static */
+std::vector<std::unique_ptr<NodeSection>>
+PolygonNodeConverter::convertHoles(std::vector<const NodeSection*>& sections)
+{
+    std::vector<std::unique_ptr<NodeSection>> convertedSections;
+    const NodeSection* copySection = sections[0];
+    for (std::size_t i = 0; i < sections.size(); i++) {
+        std::size_t inext = next(sections, i);
+        const CoordinateXY* inVertex = sections[i]->getVertex(0);
+        const CoordinateXY* outVertex = sections[inext]->getVertex(1);
+        NodeSection* ns = createSection(copySection, inVertex, outVertex);
+        convertedSections.emplace_back(ns);
+    }
+    return convertedSections;
+}
+
+
+/* private static */
+NodeSection*
+PolygonNodeConverter::createSection(
+    const NodeSection* ns,
+    const CoordinateXY* v0,
+    const CoordinateXY* v1)
+{
+    return new NodeSection(
+        ns->isA(),
+        Dimension::A,
+        ns->id(), 0,
+        ns->getPolygonal(),
+        ns->isNodeAtVertex(),
+        v0, ns->nodePt(), v1);
+}
+
+
+
+/* private static */
+std::vector<const NodeSection*>
+PolygonNodeConverter::extractUnique(std::vector<const NodeSection*>& sections)
+{
+    std::vector<const NodeSection*> uniqueSections;
+    const NodeSection* lastUnique = sections[0];
+    uniqueSections.push_back(lastUnique);
+    for (const NodeSection* ns : sections) {
+        if (0 != lastUnique->compareTo(ns)) {
+            uniqueSections.push_back(ns);
+            lastUnique = ns;
+        }
+    }
+    return uniqueSections;
+}
+
+
+/* private static */
+std::size_t
+PolygonNodeConverter::next(std::vector<const NodeSection *>& ns, std::size_t i)
+{
+    std::size_t nxt = i;
+    if (nxt == INDEX_UNKNOWN)
+        nxt = 0;
+    else
+        nxt = i + 1;
+
+    if (nxt >= ns.size())
+        nxt = 0;
+
+    return nxt;
+}
+
+
+/* private static */
+std::size_t
+PolygonNodeConverter::findShell(std::vector<const NodeSection *>& polySections)
+{
+    for (std::size_t i = 0; i < polySections.size(); i++) {
+        if (polySections[i]->isShell())
+            return i;
+    }
+    return INDEX_UNKNOWN;
+}
+
+
+} // namespace geos.operation.overlayng
+} // namespace geos.operation
+} // namespace geos
+
diff --git a/src/operation/relateng/RelateEdge.cpp b/src/operation/relateng/RelateEdge.cpp
new file mode 100644
index 000000000..4670b058b
--- /dev/null
+++ b/src/operation/relateng/RelateEdge.cpp
@@ -0,0 +1,475 @@
+/**********************************************************************
+ *
+ * GEOS - Geometry Engine Open Source
+ * http://geos.osgeo.org
+ *
+ * Copyright (c) 2024 Martin Davis
+ * Copyright (C) 2024 Paul Ramsey <pramsey at cleverelephant.ca>
+ *
+ * This is free software; you can redistribute and/or modify it under
+ * the terms of the GNU Lesser General Public Licence as published
+ * by the Free Software Foundation.
+ * See the COPYING file for more information.
+ *
+ **********************************************************************/
+
+#include <geos/algorithm/PolygonNodeTopology.h>
+#include <geos/operation/relateng/RelateEdge.h>
+#include <geos/operation/relateng/RelateNode.h>
+#include <geos/operation/relateng/RelateGeometry.h>
+#include <geos/geom/Coordinate.h>
+#include <geos/geom/Dimension.h>
+#include <geos/geom/Position.h>
+#include <geos/io/WKTWriter.h>
+#include <geos/constants.h>
+#include <sstream>
+
+using geos::algorithm::PolygonNodeTopology;
+using geos::geom::CoordinateXY;
+using geos::geom::Dimension;
+using geos::geom::Position;
+using geos::io::WKTWriter;
+
+
+namespace geos {      // geos
+namespace operation { // geos.operation
+namespace relateng {  // geos.operation.relateng
+
+
+/* public */
+RelateEdge::RelateEdge(const RelateNode* rNode, const CoordinateXY* pt, bool isA, bool isForward)
+    : node(rNode)
+    , dirPt(pt)
+{
+    setLocationsArea(isA, isForward);
+}
+
+
+/* public */
+RelateEdge::RelateEdge(const RelateNode* rNode, const CoordinateXY* pt, bool isA)
+    : node(rNode)
+    , dirPt(pt)
+{
+    setLocationsLine(isA);
+}
+
+
+/* public */
+RelateEdge::RelateEdge(const RelateNode* rNode, const CoordinateXY* pt,
+    bool isA, Location locLeft, Location locRight, Location locLine)
+    : node(rNode)
+    , dirPt(pt)
+{
+    setLocations(isA, locLeft, locRight, locLine);
+}
+
+
+/* public static */
+RelateEdge *
+RelateEdge::create(const RelateNode* node, const CoordinateXY* dirPt, bool isA, int dim, bool isForward)
+{
+    if (dim == Dimension::A)
+        //-- create an area edge
+        return new RelateEdge(node, dirPt, isA, isForward);
+    //-- create line edge
+    return new RelateEdge(node, dirPt, isA);
+}
+
+
+/* public static */
+std::size_t
+RelateEdge::findKnownEdgeIndex(
+    std::vector<std::unique_ptr<RelateEdge>>& edges,
+    bool isA)
+{
+    for (std::size_t i = 0; i < edges.size(); i++) {
+        auto& e = edges[i];
+        if (e->isKnown(isA))
+            return i;
+    }
+    return INDEX_UNKNOWN;
+}
+
+
+/* public static */
+void
+RelateEdge::setAreaInterior(
+    std::vector<std::unique_ptr<RelateEdge>>& edges,
+    bool isA)
+{
+    for (auto& e : edges) {
+        e->setAreaInterior(isA);
+    }
+}
+
+
+/* private */
+void
+RelateEdge::setLocations(bool isA, Location locLeft, Location locRight, Location locLine)
+{
+    if (isA) {
+        aDim = 2;
+        aLocLeft = locLeft;
+        aLocRight = locRight;
+        aLocLine = locLine;
+    }
+    else {
+        bDim = 2;
+        bLocLeft = locLeft;
+        bLocRight = locRight;
+        bLocLine = locLine;
+    }
+}
+
+
+/* private */
+void
+RelateEdge::setLocationsLine(bool isA)
+{
+    if (isA) {
+        aDim = 1;
+        aLocLeft = Location::EXTERIOR;
+        aLocRight = Location::EXTERIOR;
+        aLocLine = Location::INTERIOR;
+    }
+    else {
+        bDim = 1;
+        bLocLeft = Location::EXTERIOR;
+        bLocRight = Location::EXTERIOR;
+        bLocLine = Location::INTERIOR;
+    }
+}
+
+
+/* private */
+void
+RelateEdge::setLocationsArea(bool isA, bool isForward)
+{
+    Location locLeft = isForward ? Location::EXTERIOR : Location::INTERIOR;
+    Location locRight = isForward ? Location::INTERIOR : Location::EXTERIOR;
+    if (isA) {
+        aDim = 2;
+        aLocLeft = locLeft;
+        aLocRight = locRight;
+        aLocLine = Location::BOUNDARY;
+    }
+    else {
+        bDim = 2;
+        bLocLeft = locLeft;
+        bLocRight = locRight;
+        bLocLine = Location::BOUNDARY;
+    }
+}
+
+
+/* public */
+int
+RelateEdge::compareToEdge(const CoordinateXY* edgeDirPt) const
+{
+    return PolygonNodeTopology::compareAngle(
+        node->getCoordinate(),
+        dirPt,
+        edgeDirPt);
+}
+
+
+/* public */
+void
+RelateEdge::merge(bool isA, int dim, bool isForward)
+{
+    Location locEdge = Location::INTERIOR;
+    Location locLeft = Location::EXTERIOR;
+    Location locRight = Location::EXTERIOR;
+    if (dim == Dimension::A) {
+        locEdge = Location::BOUNDARY;
+        locLeft = isForward ? Location::EXTERIOR : Location::INTERIOR;
+        locRight = isForward ? Location::INTERIOR : Location::EXTERIOR;
+    }
+
+    if (! isKnown(isA)) {
+        setDimension(isA, dim);
+        setOn(isA, locEdge);
+        setLeft(isA, locLeft);
+        setRight(isA, locRight);
+        return;
+    }
+
+    // Assert: node-dirpt is collinear with node-pt
+    mergeDimEdgeLoc(isA, locEdge);
+    mergeSideLocation(isA, Position::LEFT, locLeft);
+    mergeSideLocation(isA, Position::RIGHT, locRight);
+}
+
+/**
+* Area edges override Line edges.
+* Merging edges of same dimension is a no-op for
+* the dimension and on location.
+* But merging an area edge into a line edge
+* sets the dimension to A and the location to BOUNDARY.
+*
+* @param isA
+* @param locEdge
+*/
+/* private */
+void
+RelateEdge::mergeDimEdgeLoc(bool isA, Location locEdge)
+{
+    //TODO: this logic needs work - ie handling A edges marked as Interior
+    int dim = (locEdge == Location::BOUNDARY) ? Dimension::A : Dimension::L;
+    if (dim == Dimension::A && dimension(isA) == Dimension::L) {
+        setDimension(isA, dim);
+        setOn(isA, Location::BOUNDARY);
+    }
+}
+
+
+/* private */
+void
+RelateEdge::mergeSideLocation(bool isA, int pos, Location loc)
+{
+    Location currLoc = location(isA, pos);
+    //-- INTERIOR takes precedence over EXTERIOR
+    if (currLoc != Location::INTERIOR) {
+        setLocation(isA, pos, loc);
+    }
+}
+
+
+/* private */
+void
+RelateEdge::setDimension(bool isA, int dimension)
+{
+    if (isA) {
+        aDim = dimension;
+    }
+    else {
+        bDim = dimension;
+    }
+}
+
+
+/* public */
+void
+RelateEdge::setLocation(bool isA, int pos, Location loc)
+{
+    switch (pos) {
+    case Position::LEFT:
+        setLeft(isA, loc);
+        break;
+    case Position::RIGHT:
+        setRight(isA, loc);
+        break;
+    case Position::ON:
+        setOn(isA, loc);
+        break;
+    }
+}
+
+
+/* public */
+void
+RelateEdge::setAllLocations(bool isA, Location loc)
+{
+    setLeft(isA, loc);
+    setRight(isA, loc);
+    setOn(isA, loc);
+}
+
+
+/* public */
+void
+RelateEdge::setUnknownLocations(bool isA, Location loc)
+{
+    if (! isKnown(isA, Position::LEFT)) {
+        setLocation(isA, Position::LEFT, loc);
+    }
+    if (! isKnown(isA, Position::RIGHT)) {
+        setLocation(isA, Position::RIGHT, loc);
+    }
+    if (! isKnown(isA, Position::ON)) {
+        setLocation(isA, Position::ON, loc);
+    }
+}
+
+
+/* private */
+void
+RelateEdge::setLeft(bool isA, Location loc)
+{
+    if (isA) {
+        aLocLeft = loc;
+    }
+    else {
+        bLocLeft = loc;
+    }
+}
+
+
+/* private */
+void
+RelateEdge::setRight(bool isA, Location loc)
+{
+    if (isA) {
+        aLocRight = loc;
+    }
+    else {
+        bLocRight = loc;
+    }
+}
+
+
+/* private */
+void
+RelateEdge::setOn(bool isA, Location loc)
+{
+    if (isA) {
+        aLocLine = loc;
+    }
+    else {
+        bLocLine = loc;
+    }
+}
+
+
+/* public */
+Location
+RelateEdge::location(bool isA, int position) const
+{
+    if (isA) {
+        switch (position) {
+            case Position::LEFT: return aLocLeft;
+            case Position::RIGHT: return aLocRight;
+            case Position::ON: return aLocLine;
+        }
+    }
+    else {
+        switch (position) {
+            case Position::LEFT: return bLocLeft;
+            case Position::RIGHT: return bLocRight;
+            case Position::ON: return bLocLine;
+        }
+    }
+    assert(false && "never get here");
+    return LOC_UNKNOWN;
+}
+
+/* private */
+int
+RelateEdge::dimension(bool isA) const
+{
+    return isA ? aDim : bDim;
+}
+
+/* private */
+bool
+RelateEdge::isKnown(bool isA) const
+{
+    if (isA)
+        return aDim != DIM_UNKNOWN;
+    return bDim != DIM_UNKNOWN;
+}
+
+/* private */
+bool
+RelateEdge::isKnown(bool isA, int pos) const
+{
+    return location(isA, pos) != LOC_UNKNOWN;
+}
+
+
+/* public */
+bool
+RelateEdge::isInterior(bool isA, int position) const
+{
+    return location(isA, position) == Location::INTERIOR;
+}
+
+
+/* public */
+void
+RelateEdge::setDimLocations(bool isA, int dim, Location loc)
+{
+    if (isA) {
+        aDim = dim;
+        aLocLeft = loc;
+        aLocRight = loc;
+        aLocLine = loc;
+    }
+    else {
+        bDim = dim;
+        bLocLeft = loc;
+        bLocRight = loc;
+        bLocLine = loc;
+    }
+}
+
+
+/* public */
+void
+RelateEdge::setAreaInterior(bool isA)
+{
+    if (isA) {
+        aLocLeft = Location::INTERIOR;
+        aLocRight = Location::INTERIOR;
+        aLocLine = Location::INTERIOR;
+    }
+    else {
+        bLocLeft = Location::INTERIOR;
+        bLocRight = Location::INTERIOR;
+        bLocLine = Location::INTERIOR;
+    }
+}
+
+
+/* public */
+std::string
+RelateEdge::toString() const
+{
+    std::stringstream ss;
+    ss << WKTWriter::toLineString(*(node->getCoordinate()), *dirPt);
+    ss << " - " << labelString();
+    return ss.str();
+}
+
+
+/* private */
+std::string
+RelateEdge::labelString() const
+{
+    std::stringstream ss;
+    ss << "A:";
+    ss << locationString(RelateGeometry::GEOM_A);
+    ss << "/B:";
+    ss << locationString(RelateGeometry::GEOM_B);
+    return ss.str();
+}
+
+
+/* private */
+std::string
+RelateEdge::locationString(bool isA) const
+{
+    std::stringstream ss;
+    ss << location(isA, Position::LEFT);
+    ss << location(isA, Position::ON);
+    ss << location(isA, Position::RIGHT);
+    return ss.str();
+}
+
+
+/* public friend */
+std::ostream&
+operator<<(std::ostream& os, const RelateEdge& re)
+{
+    os << re.toString();
+    return os;
+}
+
+
+} // namespace geos.operation.overlayng
+} // namespace geos.operation
+} // namespace geos
+
+
+
+
diff --git a/src/operation/relateng/RelateGeometry.cpp b/src/operation/relateng/RelateGeometry.cpp
new file mode 100644
index 000000000..94536c16e
--- /dev/null
+++ b/src/operation/relateng/RelateGeometry.cpp
@@ -0,0 +1,554 @@
+/**********************************************************************
+ *
+ * GEOS - Geometry Engine Open Source
+ * http://geos.osgeo.org
+ *
+ * Copyright (c) 2024 Martin Davis
+ * Copyright (C) 2024 Paul Ramsey <pramsey at cleverelephant.ca>
+ *
+ * This is free software; you can redistribute and/or modify it under
+ * the terms of the GNU Lesser General Public Licence as published
+ * by the Free Software Foundation.
+ * See the COPYING file for more information.
+ *
+ **********************************************************************/
+
+#include <geos/algorithm/Orientation.h>
+#include <geos/geom/Coordinate.h>
+#include <geos/geom/Dimension.h>
+#include <geos/geom/Envelope.h>
+#include <geos/geom/Geometry.h>
+#include <geos/geom/GeometryCollection.h>
+#include <geos/geom/LineString.h>
+#include <geos/geom/LinearRing.h>
+#include <geos/geom/Location.h>
+#include <geos/geom/MultiPolygon.h>
+#include <geos/geom/Point.h>
+#include <geos/geom/Polygon.h>
+#include <geos/geom/util/ComponentCoordinateExtracter.h>
+#include <geos/geom/util/PointExtracter.h>
+#include <geos/geom/util/GeometryLister.h>
+#include <geos/operation/relateng/RelateGeometry.h>
+#include <geos/operation/relateng/RelateSegmentString.h>
+#include <geos/operation/relateng/DimensionLocation.h>
+#include <geos/operation/valid/RepeatedPointRemover.h>
+
+#include <sstream>
+
+
+using geos::algorithm::BoundaryNodeRule;
+using geos::algorithm::Orientation;
+using namespace geos::geom;
+using geos::geom::util::ComponentCoordinateExtracter;
+using geos::geom::util::GeometryLister;
+using geos::geom::util::PointExtracter;
+using geos::operation::valid::RepeatedPointRemover;
+
+
+namespace geos {      // geos
+namespace operation { // geos.operation
+namespace relateng {  // geos.operation.relateng
+
+
+RelateGeometry::RelateGeometry(const Geometry* input, bool isPrepared, const BoundaryNodeRule& bnRule)
+    : geom(input)
+    , m_isPrepared(isPrepared)
+    , geomEnv(input->getEnvelopeInternal())
+    , boundaryNodeRule(bnRule)
+    , geomDim(input->getDimension())
+    , isLineZeroLen(isZeroLength(input))
+    , isGeomEmpty(input->isEmpty())
+{
+    analyzeDimensions();
+}
+
+
+/* public static */
+std::string
+RelateGeometry::name(bool isA)
+{
+    return isA ? "A" : "B";
+}
+
+
+/* private */
+void
+RelateGeometry::analyzeDimensions()
+{
+    if (isGeomEmpty) {
+        return;
+    }
+    GeometryTypeId typeId = geom->getGeometryTypeId();
+    if (typeId == GEOS_POINT || typeId == GEOS_MULTIPOINT) {
+        hasPoints = true;
+        geomDim = Dimension::P;
+        return;
+    }
+    if (typeId == GEOS_LINESTRING || typeId == GEOS_LINEARRING || typeId == GEOS_MULTILINESTRING) {
+        hasLines = true;
+        geomDim = Dimension::L;
+        return;
+    }
+    if (typeId == GEOS_POLYGON || typeId == GEOS_MULTIPOLYGON) {
+        hasAreas = true;
+        geomDim = Dimension::A;
+        return;
+    }
+    //-- analyze a (possibly mixed type) collection
+    std::vector<const Geometry*> elems;
+    GeometryLister::list(geom, elems);
+    for (const Geometry* elem : elems)
+    {
+        if (elem->isEmpty())
+            continue;
+        if (elem->getGeometryTypeId() == GEOS_POINT) {
+            hasPoints = true;
+            if (geomDim < Dimension::P) geomDim = Dimension::P;
+        }
+        if (elem->getGeometryTypeId() == GEOS_LINESTRING ||
+            elem->getGeometryTypeId() == GEOS_LINEARRING) {
+            hasLines = true;
+            if (geomDim < Dimension::L) geomDim = Dimension::L;
+        }
+        if (elem->getGeometryTypeId() == GEOS_POLYGON) {
+            hasAreas = true;
+            if (geomDim < Dimension::A) geomDim = Dimension::A;
+        }
+    }
+}
+
+
+/* private static */
+bool
+RelateGeometry::isZeroLength(const Geometry* geom)
+{
+    std::vector<const Geometry*> elems;
+    GeometryLister::list(geom, elems);
+    for (const Geometry* elem : elems) {
+        if (elem->getGeometryTypeId() == GEOS_LINESTRING ||
+            elem->getGeometryTypeId() == GEOS_LINEARRING ) {
+            if (! isZeroLength(static_cast<const LineString*>(elem))) {
+                return false;
+            }
+        }
+    }
+    return true;
+}
+
+/* private static */
+bool
+RelateGeometry::isZeroLength(const LineString* line) {
+    if (line->getNumPoints() >= 2) {
+        const CoordinateXY& p0 = line->getCoordinateN(0);
+        for (std::size_t i = 1; i < line->getNumPoints(); i++) {
+            // NOTE !!! CHANGE FROM JTS, original below
+            // const CoordinateXY& pi = line.getCoordinateN(1);
+            const CoordinateXY& pi = line->getCoordinateN(i);
+            //-- most non-zero-len lines will trigger this right away
+            if (! p0.equals2D(pi)) {
+                return false;
+            }
+        }
+    }
+    return true;
+}
+
+
+/* public */
+const Geometry*
+RelateGeometry::getGeometry() const
+{
+    return geom;
+}
+
+/* public */
+bool
+RelateGeometry::isPrepared() const
+{
+    return m_isPrepared;
+}
+
+/* public */
+const Envelope*
+RelateGeometry::getEnvelope() const
+{
+    return geomEnv;
+}
+
+/* public */
+int
+RelateGeometry::getDimension() const
+{
+    return geomDim;
+}
+
+/* public */
+bool
+RelateGeometry::hasDimension(int dim) const
+{
+    switch (dim) {
+        case Dimension::P: return hasPoints;
+        case Dimension::L: return hasLines;
+        case Dimension::A: return hasAreas;
+    }
+    return false;
+}
+
+
+/* public */
+int
+RelateGeometry::getDimensionReal() const
+{
+    if (isGeomEmpty)
+        return Dimension::False;
+    if (getDimension() == Dimension::L && isLineZeroLen)
+        return Dimension::P;
+    if (hasAreas)
+        return Dimension::A;
+    if (hasLines)
+        return Dimension::L;
+    return Dimension::P;
+}
+
+/* public */
+bool
+RelateGeometry::hasEdges() const
+{
+    return hasLines || hasAreas;
+}
+
+/* private */
+RelatePointLocator*
+RelateGeometry::getLocator()
+{
+    if (locator == nullptr)
+        locator.reset(new RelatePointLocator(geom, m_isPrepared, boundaryNodeRule));
+    return locator.get();
+}
+
+
+/* public */
+bool
+RelateGeometry::isNodeInArea(const CoordinateXY* nodePt, const Geometry* parentPolygonal)
+{
+    int dimLoc = getLocator()->locateNodeWithDim(nodePt, parentPolygonal);
+    return dimLoc == DimensionLocation::AREA_INTERIOR;
+}
+
+
+/* public */
+Location
+RelateGeometry::locateLineEnd(const CoordinateXY* p)
+{
+    return getLocator()->locateLineEnd(p);
+}
+
+
+/* public */
+Location
+RelateGeometry::locateAreaVertex(const CoordinateXY* pt)
+{
+    /**
+     * Can pass a null polygon, because the point is an exact vertex,
+     * which will be detected as being on the boundary of its polygon
+     */
+    return locateNode(pt, nullptr);
+}
+
+
+/* public */
+Location
+RelateGeometry::locateNode(const CoordinateXY* pt, const Geometry* parentPolygonal)
+{
+    return getLocator()->locateNode(pt, parentPolygonal);
+}
+
+
+/* public */
+int
+RelateGeometry::locateWithDim(const CoordinateXY* pt)
+{
+    int loc = getLocator()->locateWithDim(pt);
+    return loc;
+}
+
+
+/* public */
+bool
+RelateGeometry::isPointsOrPolygons() const
+{
+    GeometryTypeId typeId = geom->getGeometryTypeId();
+    return typeId == GEOS_POINT
+        || typeId == GEOS_MULTIPOINT
+        || typeId == GEOS_POLYGON
+        || typeId == GEOS_MULTIPOLYGON;
+}
+
+
+/* public */
+bool
+RelateGeometry::isPolygonal() const
+{
+    //TODO: also true for a GC containing one polygonal element (and possibly some lower-dimension elements)
+    GeometryTypeId typeId = geom->getGeometryTypeId();
+    return typeId == GEOS_POLYGON
+        || typeId == GEOS_MULTIPOLYGON;
+}
+
+
+/* public */
+bool
+RelateGeometry::isEmpty() const
+{
+    return isGeomEmpty;
+}
+
+
+/* public */
+bool
+RelateGeometry::hasBoundary()
+{
+    return getLocator()->hasBoundary();
+}
+
+
+/* public */
+Coordinate::ConstXYSet&
+RelateGeometry::getUniquePoints()
+{
+    if (uniquePoints.empty()) {
+        uniquePoints = createUniquePoints();
+    }
+    return uniquePoints;
+}
+
+
+/* private */
+Coordinate::ConstXYSet
+RelateGeometry::createUniquePoints()
+{
+    //-- only called on P geometries
+    std::vector<const CoordinateXY*> pts;
+    ComponentCoordinateExtracter::getCoordinates(*geom, pts);
+    Coordinate::ConstXYSet set(pts.begin(), pts.end());
+    return set;
+}
+
+
+/* public */
+std::vector<const Point*>
+RelateGeometry::getEffectivePoints()
+{
+    std::vector<const Point*> ptListAll;
+    geom::util::PointExtracter::getPoints(*geom, ptListAll);
+
+    if (getDimensionReal() <= Dimension::P)
+        return ptListAll;
+
+    //-- only return Points not covered by another element
+    std::vector<const Point*> ptList;
+    for (const Point* p : ptListAll) {
+        if (p->isEmpty())
+            continue;
+        int locDim = locateWithDim(p->getCoordinate());
+        if (DimensionLocation::dimension(locDim) == Dimension::P) {
+            ptList.push_back(p);
+        }
+    }
+    return ptList;
+}
+
+
+/* public */
+std::vector<const SegmentString*>
+RelateGeometry::extractSegmentStrings(bool isA, const Envelope* env)
+{
+    std::vector<const SegmentString*> segStrings;
+
+    // When we get called in the context of a prepared geometry
+    // geomA might already have segments extracted and stored,
+    // so check and reuse them if possible
+    if (isA && isPrepared() && env == nullptr) {
+        if (segStringPermStore.empty()) {
+            extractSegmentStrings(isA, env, geom, segStrings, segStringPermStore);
+        }
+        else {
+            for (auto& ss : segStringPermStore) {
+                segStrings.push_back(ss.get());
+            }
+        }
+    }
+    // In the context of geomB we always extract for each call,
+    // and same goes for geomA when not in prepared mode, or when
+    // using an envelope filter.
+    else {
+        segStringTempStore.clear();
+        extractSegmentStrings(isA, env, geom, segStrings, segStringTempStore);
+    }
+    return segStrings;
+}
+
+
+/* private */
+void
+RelateGeometry::extractSegmentStrings(bool isA,
+    const Envelope* env, const Geometry* p_geom,
+    std::vector<const SegmentString*>& segStrings,
+    std::vector<std::unique_ptr<const RelateSegmentString>>& segStore)
+{
+    //-- record if parent is MultiPolygon
+    const MultiPolygon* parentPolygonal = nullptr;
+    if (p_geom->getGeometryTypeId() == GEOS_MULTIPOLYGON) {
+        parentPolygonal = static_cast<const MultiPolygon*>(p_geom);
+    }
+
+    for (std::size_t i = 0; i < p_geom->getNumGeometries(); i++) {
+        const Geometry* g = p_geom->getGeometryN(i);
+        // if (g->getGeometryTypeId() == GEOS_GEOMETRYCOLLECTION) {
+        if (g->isCollection()) {
+            extractSegmentStrings(isA, env, g, segStrings, segStore);
+        }
+        else {
+            extractSegmentStringsFromAtomic(isA, g, parentPolygonal, env, segStrings, segStore);
+        }
+    }
+}
+
+
+/* private */
+void
+RelateGeometry::extractSegmentStringsFromAtomic(bool isA,
+    const Geometry* p_geom, const MultiPolygon* parentPolygonal,
+    const Envelope* env,
+    std::vector<const SegmentString*>& segStrings,
+    std::vector<std::unique_ptr<const RelateSegmentString>>& segStore)
+{
+    if (p_geom->isEmpty())
+        return;
+
+    bool doExtract = (env == nullptr) || env->intersects(p_geom->getEnvelopeInternal());
+    if (! doExtract)
+        return;
+
+    elementId++;
+    if (p_geom->getGeometryTypeId() == GEOS_LINESTRING ||
+        p_geom->getGeometryTypeId() == GEOS_LINEARRING) {
+        const LineString* line = static_cast<const LineString*>(p_geom);
+        /*
+         * Condition the input Coordinate sequence so that it has no repeated points.
+         * This requires taking a copy which removeRepeated does behind the scenes and stores in csStore.
+         */
+        const CoordinateSequence* cs = removeRepeated(line->getCoordinatesRO());
+        auto ss = RelateSegmentString::createLine(cs, isA, elementId, this);
+        segStore.emplace_back(ss);
+        segStrings.push_back(ss);
+    }
+    else if (p_geom->getGeometryTypeId() == GEOS_POLYGON) {
+        const Polygon* poly = static_cast<const Polygon*>(p_geom);
+        const Geometry* parentPoly;
+        if (parentPolygonal != nullptr)
+            parentPoly = static_cast<const Geometry*>(parentPolygonal);
+        else
+            parentPoly = static_cast<const Geometry*>(poly);
+        extractRingToSegmentString(isA, poly->getExteriorRing(), 0, env, parentPoly, segStrings, segStore);
+        for (uint32_t i = 0; i < poly->getNumInteriorRing(); i++) {
+            extractRingToSegmentString(isA, poly->getInteriorRingN(i), static_cast<int>(i+1), env, parentPoly, segStrings, segStore);
+        }
+    }
+}
+
+
+/* private */
+void
+RelateGeometry::extractRingToSegmentString(bool isA,
+    const LinearRing* ring, int ringId, const Envelope* env,
+    const Geometry* parentPoly,
+    std::vector<const SegmentString*>& segStrings,
+    std::vector<std::unique_ptr<const RelateSegmentString>>& segStore)
+{
+    if (ring->isEmpty())
+        return;
+    if (env != nullptr && ! env->intersects(ring->getEnvelopeInternal()))
+        return;
+
+    /*
+     * Condition the input Coordinate sequence so that it has no repeated points
+     * and is oriented in a deterministic way. This requires taking a copy which
+     * orientAndRemoveRepeated does behind the scenes and stores in csStore.
+     */
+    bool requireCW = (ringId == 0);
+    const CoordinateSequence* cs = orientAndRemoveRepeated(ring->getCoordinatesRO(), requireCW);
+    auto ss = RelateSegmentString::createRing(cs, isA, elementId, ringId, parentPoly, this);
+    segStore.emplace_back(ss);
+    segStrings.push_back(ss);
+}
+
+
+/* public */
+std::string
+RelateGeometry::toString() const
+{
+    return geom->toString();
+}
+
+
+/* public friend */
+std::ostream&
+operator<<(std::ostream& os, const RelateGeometry& rg)
+{
+    os << rg.toString();
+    return os;
+}
+
+
+
+/* private */
+const CoordinateSequence *
+RelateGeometry::orientAndRemoveRepeated(const CoordinateSequence *seq, bool orientCW)
+{
+    bool isFlipped = (orientCW == Orientation::isCCW(seq));
+    bool hasRepeated = seq->hasRepeatedPoints();
+    /* Already conditioned */
+    if (!isFlipped && !hasRepeated) {
+        return seq;
+    }
+
+    if (hasRepeated) {
+        auto deduped = RepeatedPointRemover::removeRepeatedPoints(seq);
+        if (isFlipped)
+            deduped->reverse();
+        CoordinateSequence* cs = deduped.release();
+        csStore.emplace_back(cs);
+        return cs;
+    }
+
+    if (isFlipped) {
+        auto reversed = seq->clone();
+        reversed->reverse();
+        CoordinateSequence* cs = reversed.release();
+        csStore.emplace_back(cs);
+        return cs;
+    }
+
+    return seq;
+}
+
+/* private */
+const CoordinateSequence *
+RelateGeometry::removeRepeated(const CoordinateSequence *seq)
+{
+    bool hasRepeated = seq->hasRepeatedPoints();
+    if (!hasRepeated)
+        return seq;
+    auto deduped = RepeatedPointRemover::removeRepeatedPoints(seq);
+    CoordinateSequence* cs = deduped.release();
+    csStore.emplace_back(cs);
+    return cs;
+}
+
+
+} // namespace geos.operation.overlayng
+} // namespace geos.operation
+} // namespace geos
+
+
diff --git a/src/operation/relateng/RelateNG.cpp b/src/operation/relateng/RelateNG.cpp
new file mode 100644
index 000000000..283f6ff19
--- /dev/null
+++ b/src/operation/relateng/RelateNG.cpp
@@ -0,0 +1,699 @@
+/**********************************************************************
+ *
+ * GEOS - Geometry Engine Open Source
+ * http://geos.osgeo.org
+ *
+ * Copyright (c) 2024 Martin Davis
+ * Copyright (C) 2024 Paul Ramsey <pramsey at cleverelephant.ca>
+ *
+ * This is free software; you can redistribute and/or modify it under
+ * the terms of the GNU Lesser General Public Licence as published
+ * by the Free Software Foundation.
+ * See the COPYING file for more information.
+ *
+ **********************************************************************/
+
+#include <geos/algorithm/BoundaryNodeRule.h>
+#include <geos/geom/Coordinate.h>
+#include <geos/geom/Dimension.h>
+#include <geos/geom/Envelope.h>
+#include <geos/geom/Geometry.h>
+#include <geos/geom/IntersectionMatrix.h>
+#include <geos/geom/LineString.h>
+#include <geos/geom/LinearRing.h>
+#include <geos/geom/Location.h>
+#include <geos/geom/Point.h>
+#include <geos/geom/Polygon.h>
+#include <geos/geom/prep/PreparedGeometry.h>
+#include <geos/geom/util/GeometryLister.h>
+#include <geos/noding/MCIndexSegmentSetMutualIntersector.h>
+#include <geos/noding/SegmentString.h>
+#include <geos/operation/relateng/DimensionLocation.h>
+#include <geos/operation/relateng/EdgeSegmentIntersector.h>
+#include <geos/operation/relateng/EdgeSetIntersector.h>
+#include <geos/operation/relateng/RelateGeometry.h>
+#include <geos/operation/relateng/RelateMatrixPredicate.h>
+#include <geos/operation/relateng/RelateNG.h>
+#include <geos/operation/relateng/RelatePredicate.h>
+#include <geos/operation/relateng/RelateSegmentString.h>
+#include <geos/operation/relateng/TopologyComputer.h>
+#include <geos/operation/relateng/TopologyPredicate.h>
+
+#include <sstream>
+
+
+using namespace geos::geom;
+using geos::algorithm::BoundaryNodeRule;
+using geos::noding::MCIndexSegmentSetMutualIntersector;
+using geos::noding::SegmentString;
+using geos::geom::util::GeometryLister;
+
+namespace geos {      // geos
+namespace operation { // geos.operation
+namespace relateng {  // geos.operation.relateng
+
+
+#define GEOM_A RelateGeometry::GEOM_A
+#define GEOM_B RelateGeometry::GEOM_B
+
+
+/************************************************************************/
+
+/* public static */
+bool
+RelateNG::intersects(const Geometry* a, const Geometry* b)
+{
+    RelatePredicate::IntersectsPredicate pred;
+    return RelateNG::relate(a, b, pred);
+}
+
+/* public static */
+bool
+RelateNG::crosses(const Geometry* a, const Geometry* b)
+{
+    RelatePredicate::CrossesPredicate pred;
+    return RelateNG::relate(a, b, pred);
+}
+
+/* public static */
+bool
+RelateNG::disjoint(const Geometry* a, const Geometry* b)
+{
+    RelatePredicate::DisjointPredicate pred;
+    return RelateNG::relate(a, b, pred);
+}
+
+/* public static */
+bool
+RelateNG::touches(const Geometry* a, const Geometry* b)
+{
+    RelatePredicate::TouchesPredicate pred;
+    return RelateNG::relate(a, b, pred);
+}
+
+/* public static */
+bool
+RelateNG::within(const Geometry* a, const Geometry* b)
+{
+    RelatePredicate::WithinPredicate pred;
+    return RelateNG::relate(a, b, pred);
+}
+
+/* public static */
+bool
+RelateNG::contains(const Geometry* a, const Geometry* b)
+{
+    RelatePredicate::ContainsPredicate pred;
+    return RelateNG::relate(a, b, pred);
+}
+
+/* public static */
+bool
+RelateNG::overlaps(const Geometry* a, const Geometry* b)
+{
+    RelatePredicate::OverlapsPredicate pred;
+    return RelateNG::relate(a, b, pred);
+}
+
+/* public static */
+bool
+RelateNG::covers(const Geometry* a, const Geometry* b)
+{
+    RelatePredicate::CoversPredicate pred;
+    return RelateNG::relate(a, b, pred);
+}
+
+/* public static */
+bool
+RelateNG::coveredBy(const Geometry* a, const Geometry* b)
+{
+    RelatePredicate::CoveredByPredicate pred;
+    return RelateNG::relate(a, b, pred);
+}
+
+/* public static */
+bool
+RelateNG::equalsTopo(const Geometry* a, const Geometry* b)
+{
+    RelatePredicate::EqualsTopoPredicate pred;
+    return RelateNG::relate(a, b, pred);
+}
+
+/* public static */
+bool
+RelateNG::relate(const Geometry* a, const Geometry* b, TopologyPredicate& pred)
+{
+    RelateNG rng(a, false);
+    return rng.evaluate(b, pred);
+}
+
+/* public static */
+bool
+RelateNG::relate(const Geometry* a, const Geometry* b, TopologyPredicate& pred, const BoundaryNodeRule& bnRule)
+{
+    RelateNG rng(a, false, bnRule);
+    return rng.evaluate(b, pred);
+}
+
+/* public static */
+bool
+RelateNG::relate(const Geometry* a, const Geometry* b, const std::string& imPattern)
+{
+    RelateNG rng(a, false);
+    return rng.evaluate(b, imPattern);
+}
+
+/* public static */
+std::unique_ptr<IntersectionMatrix>
+RelateNG::relate(const Geometry* a, const Geometry* b)
+{
+    RelateNG rng(a, false);
+    return rng.evaluate(b);
+}
+
+/* public static */
+std::unique_ptr<IntersectionMatrix>
+RelateNG::relate(const Geometry* a, const Geometry* b, const BoundaryNodeRule& bnRule)
+{
+    RelateNG rng(a, false, bnRule);
+    return rng.evaluate(b);
+}
+
+/************************************************************************/
+
+/* public */
+bool
+RelateNG::intersects(const Geometry* b)
+{
+    RelatePredicate::IntersectsPredicate pred;
+    return evaluate(b, pred);
+}
+
+/* public */
+bool
+RelateNG::crosses(const Geometry* b)
+{
+    RelatePredicate::CrossesPredicate pred;
+    return evaluate(b, pred);
+}
+
+/* public */
+bool
+RelateNG::disjoint(const Geometry* b)
+{
+    RelatePredicate::DisjointPredicate pred;
+    return evaluate(b, pred);
+}
+
+/* public */
+bool
+RelateNG::touches(const Geometry* b)
+{
+    RelatePredicate::TouchesPredicate pred;
+    return evaluate(b, pred);
+}
+
+/* public */
+bool
+RelateNG::within(const Geometry* a)
+{
+    RelatePredicate::WithinPredicate pred;
+    return evaluate(a, pred);
+}
+
+/* public */
+bool
+RelateNG::contains(const Geometry* b)
+{
+    RelatePredicate::ContainsPredicate pred;
+    return evaluate(b, pred);
+}
+
+/* public */
+bool
+RelateNG::overlaps(const Geometry* b)
+{
+    RelatePredicate::OverlapsPredicate pred;
+    return evaluate(b, pred);
+}
+
+/* public */
+bool
+RelateNG::covers(const Geometry* b)
+{
+    RelatePredicate::CoversPredicate pred;
+    return evaluate(b, pred);
+}
+
+/* public */
+bool
+RelateNG::coveredBy(const Geometry* b)
+{
+    RelatePredicate::CoveredByPredicate pred;
+    return evaluate(b, pred);
+}
+
+/* public */
+bool
+RelateNG::equalsTopo(const Geometry* b)
+{
+    RelatePredicate::EqualsTopoPredicate pred;
+    return evaluate(b, pred);
+}
+
+/* public */
+bool
+RelateNG::relate(const Geometry* b, const std::string& imPattern)
+{
+    return evaluate(b, imPattern);
+}
+
+/* public */
+std::unique_ptr<IntersectionMatrix>
+RelateNG::relate(const Geometry* b)
+{
+    return evaluate(b);
+}
+
+/************************************************************************/
+
+/* public static */
+std::unique_ptr<RelateNG>
+RelateNG::prepare(const Geometry* a)
+{
+    return std::unique_ptr<RelateNG>(new RelateNG(a, true));
+}
+
+
+/* public static */
+std::unique_ptr<RelateNG>
+RelateNG::prepare(const Geometry* a, const BoundaryNodeRule& bnRule)
+{
+    return std::unique_ptr<RelateNG>(new RelateNG(a, true, bnRule));
+}
+
+/************************************************************************/
+
+/* public */
+std::unique_ptr<IntersectionMatrix>
+RelateNG::evaluate(const Geometry* b)
+{
+    RelateMatrixPredicate rel;
+    evaluate(b, rel);
+    return rel.getIM();
+}
+
+
+/* public */
+bool
+RelateNG::evaluate(const Geometry* b, const std::string& imPattern)
+{
+    auto predicate = RelatePredicate::matches(imPattern);
+    return evaluate(b, *predicate);
+}
+
+
+/* public */
+bool
+RelateNG::evaluate(const Geometry* b, TopologyPredicate& predicate)
+{
+    //-- fast envelope checks
+    if (! hasRequiredEnvelopeInteraction(b, predicate)) {
+        return false;
+    }
+
+    geos::util::ensureNoCurvedComponents(geomA.getGeometry());
+    geos::util::ensureNoCurvedComponents(b);
+    
+    RelateGeometry geomB(b, boundaryNodeRule);
+
+    int dimA = geomA.getDimensionReal();
+    int dimB = geomB.getDimensionReal();
+
+    //-- check if predicate is determined by dimension or envelope
+    predicate.init(dimA, dimB);
+    if (predicate.isKnown())
+        return finishValue(predicate);
+
+    predicate.init(*(geomA.getEnvelope()), *(geomB.getEnvelope()));
+    if (predicate.isKnown())
+        return finishValue(predicate);
+
+    TopologyComputer topoComputer(predicate, geomA, geomB);
+
+    //-- optimized P/P evaluation
+    if (dimA == Dimension::P && dimB == Dimension::P) {
+        computePP(geomB, topoComputer);
+        topoComputer.finish();
+        return topoComputer.getResult();
+    }
+
+    //-- test points against (potentially) indexed geometry first
+    computeAtPoints(geomB, GEOM_B, geomA, topoComputer);
+    if (topoComputer.isResultKnown()) {
+        return topoComputer.getResult();
+    }
+    computeAtPoints(geomA, GEOM_A, geomB, topoComputer);
+    if (topoComputer.isResultKnown()) {
+        return topoComputer.getResult();
+    }
+
+    if (geomA.hasEdges() && geomB.hasEdges()) {
+        computeAtEdges(geomB, topoComputer);
+    }
+
+    //-- after all processing, set remaining unknown values in IM
+    topoComputer.finish();
+    return topoComputer.getResult();
+}
+
+
+/* private */
+bool
+RelateNG::hasRequiredEnvelopeInteraction(const Geometry* b, TopologyPredicate& predicate)
+{
+    const Envelope* envB = b->getEnvelopeInternal();
+    bool isInteracts = false;
+    if (predicate.requireCovers(GEOM_A)) {
+        if (! geomA.getEnvelope()->covers(envB)) {
+            return false;
+        }
+        isInteracts = true;
+    }
+    else if (predicate.requireCovers(GEOM_B)) {
+        if (! envB->covers(geomA.getEnvelope())) {
+            return false;
+        }
+        isInteracts = true;
+    }
+    if (! isInteracts
+        && predicate.requireInteraction()
+        && ! geomA.getEnvelope()->intersects(envB)) {
+        return false;
+    }
+    return true;
+}
+
+/* private */
+bool
+RelateNG::finishValue(TopologyPredicate& predicate)
+{
+    predicate.finish();
+    return predicate.value();
+}
+
+
+/* private */
+void
+RelateNG::computePP(RelateGeometry& geomB, TopologyComputer& topoComputer)
+{
+    Coordinate::ConstXYSet& ptsA = geomA.getUniquePoints();
+    //TODO: only query points in interaction extent?
+    Coordinate::ConstXYSet& ptsB = geomB.getUniquePoints();
+
+    uint32_t numBinA = 0;
+    for (const CoordinateXY* ptB : ptsB) {
+        auto it = ptsA.find(ptB);
+        bool found = (it != ptsA.end());
+        if (found) {
+            numBinA++;
+            topoComputer.addPointOnPointInterior(ptB);
+        }
+        else {
+            topoComputer.addPointOnPointExterior(GEOM_B, ptB);
+        }
+        if (topoComputer.isResultKnown()) {
+            return;
+        }
+    }
+    /**
+     * If number of matched B points is less than size of A,
+     * there must be at least one A point in the exterior of B
+     */
+    if (numBinA < ptsA.size()) {
+        //TODO: determine actual exterior point?
+        topoComputer.addPointOnPointExterior(GEOM_A, nullptr);
+    }
+}
+
+
+/* private */
+void
+RelateNG::computeAtPoints(
+    RelateGeometry& geom, bool isA,
+    RelateGeometry& geomTarget, TopologyComputer& topoComputer)
+{
+    bool isResultKnown = false;
+    isResultKnown = computePoints(geom, isA, geomTarget, topoComputer);
+    if (isResultKnown)
+        return;
+
+    /**
+     * Performance optimization: only check points against target
+     * if it has areas OR if the predicate requires checking for
+     * exterior interaction.
+     * In particular, this avoids testing line ends against lines
+     * for the intersects predicate (since these are checked
+     * during segment/segment intersection checking anyway).
+     * Checking points against areas is necessary, since the input
+     * linework is disjoint if one input lies wholly inside an area,
+     * so segment intersection checking is not sufficient.
+     */
+    bool checkDisjointPoints = geomTarget.hasDimension(Dimension::A)
+                                || topoComputer.isExteriorCheckRequired(isA);
+    if (! checkDisjointPoints)
+        return;
+
+    isResultKnown = computeLineEnds(geom, isA, geomTarget, topoComputer);
+    if (isResultKnown)
+        return;
+
+    computeAreaVertex(geom, isA, geomTarget, topoComputer);
+}
+
+
+/* private */
+bool
+RelateNG::computePoints(
+    RelateGeometry& geom, bool isA,
+    RelateGeometry& geomTarget, TopologyComputer& topoComputer)
+{
+    if (! geom.hasDimension(Dimension::P)) {
+        return false;
+    }
+
+    std::vector<const Point*> points = geom.getEffectivePoints();
+    for (const Point* point : points) {
+        //TODO: exit when all possible target locations (E,I,B) have been found?
+        if (point->isEmpty())
+            continue;
+
+        const CoordinateXY* pt = point->getCoordinate();
+        computePoint(isA, pt, geomTarget, topoComputer);
+        if (topoComputer.isResultKnown()) {
+            return true;
+        }
+    }
+    return false;
+}
+
+
+/* private */
+void
+RelateNG::computePoint(bool isA, const CoordinateXY* pt, RelateGeometry& geomTarget, TopologyComputer& topoComputer)
+{
+      int locDimTarget = geomTarget.locateWithDim(pt);
+      Location locTarget = DimensionLocation::location(locDimTarget);
+      int dimTarget = DimensionLocation::dimension(locDimTarget, topoComputer.getDimension(! isA));
+      topoComputer.addPointOnGeometry(isA, locTarget, dimTarget, pt);
+}
+
+
+/* private */
+bool
+RelateNG::computeLineEnds(
+    RelateGeometry& geom, bool isA,
+    RelateGeometry& geomTarget, TopologyComputer& topoComputer)
+{
+    if (! geom.hasDimension(Dimension::L)) {
+        return false;
+    }
+
+    bool hasExteriorIntersection = false;
+    std::vector<const Geometry*> elems;
+    GeometryLister::list(geom.getGeometry(), elems);
+    for (const Geometry* elem : elems) {
+        if (elem->isEmpty())
+            continue;
+
+        if (elem->getGeometryTypeId() == GEOS_LINESTRING ||
+            elem->getGeometryTypeId() == GEOS_LINEARRING) {
+            //-- once an intersection with target exterior is recorded, skip further known-exterior points
+            if (hasExteriorIntersection
+                && elem->getEnvelopeInternal()->disjoint(geomTarget.getEnvelope()))
+                continue;
+
+            const LineString* line = static_cast<const LineString*>(elem);
+            //TODO: add optimzation to skip disjoint elements once exterior point found
+            const CoordinateXY& e0 = line->getCoordinatesRO()->getAt(0);
+            hasExteriorIntersection |= computeLineEnd(geom, isA, &e0, geomTarget, topoComputer);
+            if (topoComputer.isResultKnown()) {
+                return true;
+            }
+
+            if (! line->isClosed()) {
+                const CoordinateXY& e1 = line->getCoordinatesRO()->getAt(line->getNumPoints() - 1);
+                hasExteriorIntersection |= computeLineEnd(geom, isA, &e1, geomTarget, topoComputer);
+                if (topoComputer.isResultKnown()) {
+                    return true;
+                }
+            }
+        //TODO: break when all possible locations have been found?
+        }
+    }
+    return false;
+}
+
+
+/* private */
+bool
+RelateNG::computeLineEnd(RelateGeometry& geom, bool isA, const CoordinateXY* pt,
+      RelateGeometry& geomTarget, TopologyComputer& topoComputer)
+{
+    Location locLineEnd = geom.locateLineEnd(pt);
+    int locDimTarget = geomTarget.locateWithDim(pt);
+    Location locTarget = DimensionLocation::location(locDimTarget);
+    int dimTarget = DimensionLocation::dimension(locDimTarget, topoComputer.getDimension(! isA));
+    topoComputer.addLineEndOnGeometry(isA, locLineEnd, locTarget, dimTarget, pt);
+    return locTarget == Location::EXTERIOR;
+}
+
+
+/* private */
+bool
+RelateNG::computeAreaVertex(RelateGeometry& geom, bool isA, RelateGeometry& geomTarget, TopologyComputer& topoComputer)
+{
+    if (! geom.hasDimension(Dimension::A)) {
+        return false;
+    }
+    //-- evaluate for line and area targets only, since points are handled in the reverse direction
+    if (geomTarget.getDimension() < Dimension::L)
+        return false;
+
+    bool hasExteriorIntersection = false;
+
+    std::vector<const Geometry*> elems;
+    GeometryLister::list(geom.getGeometry(), elems);
+    for (const Geometry* elem : elems) {
+        if (elem->isEmpty())
+            continue;
+
+        if (elem->getGeometryTypeId() == GEOS_POLYGON) {
+            //-- once an intersection with target exterior is recorded, skip further known-exterior points
+            if (hasExteriorIntersection && elem->getEnvelopeInternal()->disjoint(geomTarget.getEnvelope()))
+                continue;
+
+            const Polygon* poly = static_cast<const Polygon*>(elem);
+            hasExteriorIntersection |= computeAreaVertex(geom, isA, poly->getExteriorRing(), geomTarget, topoComputer);
+            if (topoComputer.isResultKnown()) {
+                return true;
+            }
+            for (std::size_t j = 0; j < poly->getNumInteriorRing(); j++) {
+                hasExteriorIntersection |= computeAreaVertex(geom, isA, poly->getInteriorRingN(j), geomTarget, topoComputer);
+                if (topoComputer.isResultKnown()) {
+                    return true;
+                }
+            }
+        }
+    }
+    return false;
+}
+
+
+/* private */
+bool
+RelateNG::computeAreaVertex(RelateGeometry& geom, bool isA, const LinearRing* ring, RelateGeometry& geomTarget, TopologyComputer& topoComputer)
+{
+    //TODO: use extremal (highest) point to ensure one is on boundary of polygon cluster
+    const CoordinateXY* pt = ring->getCoordinate();
+
+    Location locArea = geom.locateAreaVertex(pt);
+    int locDimTarget = geomTarget.locateWithDim(pt);
+    Location locTarget = DimensionLocation::location(locDimTarget);
+    int dimTarget = DimensionLocation::dimension(locDimTarget, topoComputer.getDimension(! isA));
+    topoComputer.addAreaVertex(isA, locArea, locTarget, dimTarget, pt);
+    return locTarget == Location::EXTERIOR;
+}
+
+
+/* private */
+void
+RelateNG::computeAtEdges(RelateGeometry& geomB, TopologyComputer& topoComputer)
+{
+    Envelope envInt;
+    geomA.getEnvelope()->intersection(*(geomB.getEnvelope()), envInt);
+    if (envInt.isNull())
+        return;
+
+    std::vector<const SegmentString*> edgesB = geomB.extractSegmentStrings(GEOM_B, &envInt);
+    EdgeSegmentIntersector intersector(topoComputer);
+
+    if (topoComputer.isSelfNodingRequired()) {
+        computeEdgesAll(edgesB, &envInt, intersector);
+    }
+    else {
+        computeEdgesMutual(edgesB, &envInt, intersector);
+    }
+    if (topoComputer.isResultKnown()) {
+        return;
+    }
+
+    topoComputer.evaluateNodes();
+}
+
+
+/* private */
+void
+RelateNG::computeEdgesAll(std::vector<const SegmentString*>& edgesB, const Envelope* envInt, EdgeSegmentIntersector& intersector)
+{
+    //TODO: find a way to reuse prepared index?
+    std::vector<const SegmentString*> edgesA = geomA.extractSegmentStrings(GEOM_A, envInt);
+
+    EdgeSetIntersector edgeInt(edgesA, edgesB, envInt);
+    edgeInt.process(intersector);
+}
+
+
+/* private */
+void
+RelateNG::computeEdgesMutual(std::vector<const SegmentString*>& edgesB, const Envelope* envInt, EdgeSegmentIntersector& intersector)
+{
+    //-- in prepared mode the A edge index is reused
+    if (edgeMutualInt == nullptr) {
+        const Envelope* envExtract = geomA.isPrepared() ? nullptr : envInt;
+        std::vector<const SegmentString*> edgesA = geomA.extractSegmentStrings(GEOM_A, envExtract);
+        edgeMutualInt.reset(new MCIndexSegmentSetMutualIntersector(envExtract));
+        edgeMutualInt->setBaseSegments(&edgesA);
+
+    }
+
+    edgeMutualInt->setSegmentIntersector(&intersector);
+    edgeMutualInt->process(&edgesB);
+}
+
+
+
+
+
+
+
+
+
+
+
+
+} // namespace geos.operation.overlayng
+} // namespace geos.operation
+} // namespace geos
+
+
diff --git a/src/operation/relateng/RelateNode.cpp b/src/operation/relateng/RelateNode.cpp
new file mode 100644
index 000000000..0ec3045a6
--- /dev/null
+++ b/src/operation/relateng/RelateNode.cpp
@@ -0,0 +1,323 @@
+/**********************************************************************
+ *
+ * GEOS - Geometry Engine Open Source
+ * http://geos.osgeo.org
+ *
+ * Copyright (c) 2024 Martin Davis
+ * Copyright (C) 2024 Paul Ramsey <pramsey at cleverelephant.ca>
+ *
+ * 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/operation/relateng/RelateNode.h>
+#include <geos/operation/relateng/RelateEdge.h>
+#include <geos/operation/relateng/RelateGeometry.h>
+#include <geos/operation/relateng/NodeSection.h>
+#include <geos/geom/Dimension.h>
+#include <geos/geom/Position.h>
+#include <geos/geom/Coordinate.h>
+#include <geos/io/WKTWriter.h>
+#include <geos/constants.h>
+
+
+using geos::geom::CoordinateXY;
+using geos::geom::Geometry;
+using geos::geom::Dimension;
+using geos::geom::Position;
+using geos::io::WKTWriter;
+
+
+namespace geos {      // geos
+namespace operation { // geos.operation
+namespace relateng {  // geos.operation.relateng
+
+
+/* public */
+const CoordinateXY*
+RelateNode::getCoordinate() const
+{
+    return nodePt;
+}
+
+
+/* public */
+const std::vector<std::unique_ptr<RelateEdge>>&
+RelateNode::getEdges() const
+{
+    return edges;
+}
+
+
+/* public */
+void
+RelateNode::addEdges(std::vector<const NodeSection *>& nss)
+{
+    for (auto* ns : nss) {
+        addEdges(ns);
+    }
+}
+
+/* public */
+void
+RelateNode::addEdges(std::vector<std::unique_ptr<NodeSection>>& nss)
+{
+    for (auto& ns : nss) {
+        addEdges(ns.get());
+    }
+}
+
+/* private */
+std::size_t
+RelateNode::indexOf(
+    const std::vector<std::unique_ptr<RelateEdge>>& vEdges,
+    const RelateEdge* edge) const
+{
+    for (std::size_t i = 0; i < vEdges.size(); i++)
+    {
+        const std::unique_ptr<RelateEdge>& e = vEdges[i];
+        if (e.get() == edge)
+            return i;
+    }
+    return INDEX_UNKNOWN;
+}
+
+
+/* public */
+void
+RelateNode::addEdges(const NodeSection* ns)
+{
+  //Debug.println("Adding NS: " + ns);
+    switch (ns->dimension()) {
+    case Dimension::L:
+        addLineEdge(ns->isA(), ns->getVertex(0));
+        addLineEdge(ns->isA(), ns->getVertex(1));
+        break;
+    case Dimension::A:
+        //-- assumes node edges have CW orientation (as per JTS norm)
+        //-- entering edge - interior on L
+        const RelateEdge* e0 = addAreaEdge(ns->isA(), ns->getVertex(0), false);
+        //-- exiting edge - interior on R
+        const RelateEdge* e1 = addAreaEdge(ns->isA(), ns->getVertex(1), true);
+
+        std::size_t index0 = indexOf(edges, e0);
+        std::size_t index1 = indexOf(edges, e1);
+        updateEdgesInArea(ns->isA(), index0, index1);
+        updateIfAreaPrev(ns->isA(), index0);
+        updateIfAreaNext(ns->isA(), index1);
+    }
+}
+
+
+/* private */
+void
+RelateNode::updateEdgesInArea(bool isA, std::size_t indexFrom, std::size_t indexTo)
+{
+    std::size_t index = nextIndex(edges, indexFrom);
+    while (index != indexTo) {
+        auto& edge = edges[index];
+        edge->setAreaInterior(isA);
+        index = nextIndex(edges, index);
+    }
+}
+
+
+/* private */
+void
+RelateNode::updateIfAreaPrev(bool isA, std::size_t index)
+{
+    std::size_t indexPrev = prevIndex(edges, index);
+    auto& edgePrev = edges[indexPrev];
+    if (edgePrev->isInterior(isA, Position::LEFT)) {
+        std::unique_ptr<RelateEdge>& edge = edges[index];
+        edge->setAreaInterior(isA);
+    }
+}
+
+
+/* private */
+void
+RelateNode::updateIfAreaNext(bool isA, std::size_t index)
+{
+    std::size_t indexNext = nextIndex(edges, index);
+    auto& edgeNext = edges[indexNext];
+    if (edgeNext->isInterior(isA, Position::RIGHT)) {
+        auto& edge = edges[index];
+        edge->setAreaInterior(isA);
+    }
+}
+
+
+/* private */
+const RelateEdge*
+RelateNode::addLineEdge(bool isA, const CoordinateXY* dirPt)
+{
+    return addEdge(isA, dirPt, Dimension::L, false);
+}
+
+
+/* private */
+const RelateEdge*
+RelateNode::addAreaEdge(bool isA, const CoordinateXY* dirPt, bool isForward)
+{
+    return addEdge(isA, dirPt, Dimension::A, isForward);
+}
+
+
+/* private */
+const RelateEdge*
+RelateNode::addEdge(bool isA, const CoordinateXY* dirPt, int dim, bool isForward)
+{
+    //-- check for well-formed edge - skip null or zero-len input
+    if (dirPt == nullptr)
+        return nullptr;
+    if (nodePt->equals2D(*dirPt))
+        return nullptr;
+
+    std::size_t insertIndex = INDEX_UNKNOWN;
+    for (std::size_t i = 0; i < edges.size(); i++) {
+        auto* e = edges[i].get();
+        int comp = e->compareToEdge(dirPt);
+        if (comp == 0) {
+            e->merge(isA, dim, isForward);
+            return e;
+        }
+        if (comp == 1) {
+            //-- found further edge, so insert a new edge at this position
+            insertIndex = i;
+            break;
+        }
+    }
+    //-- add a new edge
+    RelateEdge* e = RelateEdge::create(this, dirPt, isA, dim, isForward);
+    if (insertIndex == INDEX_UNKNOWN) {
+        //-- add edge at end of list
+        edges.emplace_back(e);
+    }
+    else {
+        //-- add edge before higher edge found
+        std::unique_ptr<RelateEdge> re(e);
+        edges.insert(
+            edges.begin() + static_cast<long>(insertIndex),
+            std::move(re));
+    }
+    return e;
+}
+
+
+/* public */
+void
+RelateNode::finish(bool isAreaInteriorA, bool isAreaInteriorB)
+{
+    //Debug.println("finish Node.");
+    //Debug.println("Before: " + this);
+    finishNode(RelateGeometry::GEOM_A, isAreaInteriorA);
+    finishNode(RelateGeometry::GEOM_B, isAreaInteriorB);
+    //Debug.println("After: " + this);
+}
+
+
+/* private */
+void
+RelateNode::finishNode(bool isA, bool isAreaInterior)
+{
+    if (isAreaInterior) {
+        RelateEdge::setAreaInterior(edges, isA);
+    }
+    else {
+        std::size_t startIndex = RelateEdge::findKnownEdgeIndex(edges, isA);
+        //-- only interacting nodes are finished, so this should never happen
+        //Assert.isTrue(startIndex >= 0l, "Node at "+ nodePt + "does not have AB interaction");
+        propagateSideLocations(isA, startIndex);
+    }
+}
+
+
+/* private */
+void
+RelateNode::propagateSideLocations(bool isA, std::size_t startIndex)
+{
+    Location currLoc = edges[startIndex]->location(isA, Position::LEFT);
+    //-- edges are stored in CCW order
+    std::size_t index = nextIndex(edges, startIndex);
+    while (index != startIndex) {
+        const auto& e = edges[index];
+        e->setUnknownLocations(isA, currLoc);
+        currLoc = e->location(isA, Position::LEFT);
+        index = nextIndex(edges, index);
+    }
+}
+
+
+/* private static */
+std::size_t
+RelateNode::prevIndex(
+    std::vector<std::unique_ptr<RelateEdge>>& list,
+    std::size_t index)
+{
+    if (index > 0 && index != INDEX_UNKNOWN) {
+        return index - 1;
+    }
+    //-- index == 0
+    return list.size() - 1;
+}
+
+
+/* private static */
+std::size_t
+RelateNode::nextIndex(
+    std::vector<std::unique_ptr<RelateEdge>>& list,
+    std::size_t index)
+{
+    if (index >= list.size() - 1 || index == INDEX_UNKNOWN) {
+        return 0;
+    }
+    return index + 1;
+}
+
+
+/* public */
+bool
+RelateNode::hasExteriorEdge(bool isA)
+{
+    for (auto& e : edges) {
+        if (Location::EXTERIOR == e->location(isA, Position::LEFT) ||
+            Location::EXTERIOR == e->location(isA, Position::RIGHT))
+        {
+            return true;
+        }
+    }
+    return false;
+}
+
+
+/* public */
+std::string
+RelateNode::toString() const
+{
+    std::stringstream ss;
+    ss << "Node[" << WKTWriter::toPoint(*nodePt) << "]:" << std::endl;
+    for (auto& e : edges) {
+        ss << e->toString() << std::endl;
+    }
+    return ss.str();
+}
+
+
+/* public friend */
+std::ostream&
+operator<<(std::ostream& os, const RelateNode& rn)
+{
+    os << rn.toString();
+    return os;
+}
+
+
+} // namespace geos.operation.overlayng
+} // namespace geos.operation
+} // namespace geos
+
+
diff --git a/src/operation/relateng/RelatePointLocator.cpp b/src/operation/relateng/RelatePointLocator.cpp
new file mode 100644
index 000000000..cca69d8ec
--- /dev/null
+++ b/src/operation/relateng/RelatePointLocator.cpp
@@ -0,0 +1,331 @@
+/**********************************************************************
+ *
+ * GEOS - Geometry Engine Open Source
+ * http://geos.osgeo.org
+ *
+ * Copyright (c) 2024 Martin Davis
+ * Copyright (C) 2024 Paul Ramsey <pramsey at cleverelephant.ca>
+ *
+ * This is free software; you can redistribute and/or modify it under
+ * the terms of the GNU Lesser General Public Licence as published
+ * by the Free Software Foundation.
+ * See the COPYING file for more information.
+ *
+ **********************************************************************/
+
+#include <geos/algorithm/PointLocation.h>
+#include <geos/algorithm/locate/IndexedPointInAreaLocator.h>
+#include <geos/algorithm/locate/PointOnGeometryLocator.h>
+#include <geos/algorithm/locate/SimplePointInAreaLocator.h>
+#include <geos/geom/Coordinate.h>
+#include <geos/geom/CoordinateSequence.h>
+#include <geos/geom/Geometry.h>
+#include <geos/geom/GeometryCollection.h>
+#include <geos/geom/LineString.h>
+#include <geos/geom/Location.h>
+#include <geos/geom/MultiPolygon.h>
+#include <geos/geom/Point.h>
+#include <geos/geom/Polygon.h>
+#include <geos/operation/relateng/AdjacentEdgeLocator.h>
+#include <geos/operation/relateng/DimensionLocation.h>
+#include <geos/operation/relateng/LinearBoundary.h>
+#include <geos/operation/relateng/RelatePointLocator.h>
+#include <geos/constants.h>
+
+
+using geos::algorithm::PointLocation;
+using geos::algorithm::locate::IndexedPointInAreaLocator;
+using geos::algorithm::locate::PointOnGeometryLocator;
+using geos::algorithm::locate::SimplePointInAreaLocator;
+using namespace geos::geom;
+
+
+namespace geos {      // geos
+namespace operation { // geos.operation
+namespace relateng {  // geos.operation.relateng
+
+
+/* private */
+void
+RelatePointLocator::init(const Geometry* p_geom)
+{
+    //-- cache empty status, since may be checked many times
+    isEmpty = p_geom->isEmpty();
+    extractElements(p_geom);
+
+    if (!lines.empty()) {
+        lineBoundary.reset(new LinearBoundary(lines, boundaryRule));
+    }
+
+    if (!polygons.empty()) {
+        polyLocator.resize(polygons.size());
+    }
+}
+
+
+/* public */
+bool
+RelatePointLocator::hasBoundary() const
+{
+    return lineBoundary->hasBoundary();
+}
+
+
+/* private */
+void
+RelatePointLocator::extractElements(const Geometry* p_geom)
+{
+    if (p_geom->isEmpty())
+        return;
+
+    if (p_geom->getGeometryTypeId() == GEOS_POINT) {
+        addPoint(static_cast<const Point*>(p_geom));
+    }
+    else if (p_geom->getGeometryTypeId() == GEOS_LINESTRING ||
+             p_geom->getGeometryTypeId() == GEOS_LINEARRING) {
+        addLine(static_cast<const LineString*>(p_geom));
+    }
+    else if (p_geom->getGeometryTypeId() == GEOS_POLYGON ||
+             p_geom->getGeometryTypeId() == GEOS_MULTIPOLYGON)
+    {
+        addPolygonal(p_geom);
+    }
+    else if (p_geom->isCollection()) {
+        for (std::size_t i = 0; i < p_geom->getNumGeometries(); i++) {
+            const Geometry* g = p_geom->getGeometryN(i);
+            extractElements(g);
+        }
+    }
+}
+
+
+/* private */
+void
+RelatePointLocator::addPoint(const Point* pt)
+{
+    points.insert(pt->getCoordinate());
+}
+
+
+/* private */
+void
+RelatePointLocator::addLine(const LineString* line)
+{
+    lines.push_back(line);
+}
+
+
+/* private */
+void
+RelatePointLocator::addPolygonal(const Geometry* polygonal)
+{
+    polygons.push_back(polygonal);
+}
+
+
+/* public */
+Location
+RelatePointLocator::locate(const CoordinateXY* p)
+{
+    return DimensionLocation::location(locateWithDim(p));
+}
+
+
+/* public */
+Location
+RelatePointLocator::locateLineEnd(const CoordinateXY* p) const
+{
+    return lineBoundary->isBoundary(p) ? Location::BOUNDARY : Location::INTERIOR;
+}
+
+
+/* public */
+Location
+RelatePointLocator::locateNode(const CoordinateXY* p, const Geometry* parentPolygonal)
+{
+    return DimensionLocation::location(locateNodeWithDim(p, parentPolygonal));
+}
+
+
+/* public */
+int
+RelatePointLocator::locateNodeWithDim(const CoordinateXY* p, const Geometry* parentPolygonal)
+{
+    return locateWithDim(p, true, parentPolygonal);
+}
+
+
+/* public */
+int
+RelatePointLocator::locateWithDim(const CoordinateXY* p)
+{
+    return locateWithDim(p, false, nullptr);
+}
+
+
+/* private */
+int
+RelatePointLocator::locateWithDim(const CoordinateXY* p, bool isNode, const Geometry* parentPolygonal)
+{
+    if (isEmpty) return DimensionLocation::EXTERIOR;
+
+    /**
+     * In a polygonal geometry a node must be on the boundary.
+     * (This is not the case for a mixed collection, since
+     * the node may be in the interior of a polygon.)
+     */
+    GeometryTypeId geomType = geom->getGeometryTypeId();
+    if (isNode && (geomType == GEOS_POLYGON || geomType == GEOS_MULTIPOLYGON))
+        return DimensionLocation::AREA_BOUNDARY;
+
+    int dimLoc = computeDimLocation(p, isNode, parentPolygonal);
+    return dimLoc;
+}
+
+
+/* private */
+int
+RelatePointLocator::computeDimLocation(const CoordinateXY* p, bool isNode, const Geometry* parentPolygonal)
+{
+    //-- check dimensions in order of precedence
+    if (!polygons.empty()) {
+        Location locPoly = locateOnPolygons(p, isNode, parentPolygonal);
+        if (locPoly != Location::EXTERIOR)
+            return DimensionLocation::locationArea(locPoly);
+    }
+    if (!lines.empty()) {
+        Location locLine = locateOnLines(p, isNode);
+        if (locLine != Location::EXTERIOR)
+            return DimensionLocation::locationLine(locLine);
+    }
+    if (!points.empty()) {
+        Location locPt = locateOnPoints(p);
+        if (locPt != Location::EXTERIOR)
+            return DimensionLocation::locationPoint(locPt);
+    }
+    return DimensionLocation::EXTERIOR;
+}
+
+
+/* private */
+Location
+RelatePointLocator::locateOnPoints(const CoordinateXY* p) const
+{
+    auto search = points.find(p);
+    if (search != points.end())
+        return Location::INTERIOR;
+    else
+        return Location::EXTERIOR;
+}
+
+
+/* private */
+Location
+RelatePointLocator::locateOnLines(const CoordinateXY* p, bool isNode)
+{
+    if (lineBoundary != nullptr && lineBoundary->isBoundary(p)) {
+        return Location::BOUNDARY;
+    }
+    //-- must be on line, in interior
+    if (isNode)
+        return Location::INTERIOR;
+
+    //TODO: index the lines
+    for (const LineString* line : lines) {
+        //-- have to check every line, since any/all may contain point
+        Location loc = locateOnLine(p, /*isNode,*/ line);
+        if (loc != Location::EXTERIOR)
+            return loc;
+        //TODO: minor optimization - some BoundaryNodeRules can short-circuit
+    }
+    return Location::EXTERIOR;
+}
+
+
+/* private */
+Location
+RelatePointLocator::locateOnLine(const CoordinateXY* p, /*bool isNode,*/ const LineString* l)
+{
+    // bounding-box check
+    if (! l->getEnvelopeInternal()->intersects(*p))
+        return Location::EXTERIOR;
+
+    const CoordinateSequence* seq = l->getCoordinatesRO();
+    if (PointLocation::isOnLine(*p, seq)) {
+        return Location::INTERIOR;
+    }
+    return Location::EXTERIOR;
+}
+
+
+/* private */
+Location
+RelatePointLocator::locateOnPolygons(const CoordinateXY* p, bool isNode, const Geometry* parentPolygonal)
+{
+    int numBdy = 0;
+    //TODO: use a spatial index on the polygons
+    for (std::size_t i = 0; i < polygons.size(); i++) {
+        Location loc = locateOnPolygonal(p, isNode, parentPolygonal, i);
+        if (loc == Location::INTERIOR) {
+            return Location::INTERIOR;
+        }
+        if (loc == Location::BOUNDARY) {
+            numBdy += 1;
+        }
+    }
+    if (numBdy == 1) {
+        return Location::BOUNDARY;
+    }
+    //-- check for point lying on adjacent boundaries
+    else if (numBdy > 1) {
+        if (adjEdgeLocator == nullptr) {
+            adjEdgeLocator.reset(new AdjacentEdgeLocator(geom));
+        }
+        return adjEdgeLocator->locate(p);
+    }
+    return Location::EXTERIOR;
+}
+
+
+/* private */
+Location
+RelatePointLocator::locateOnPolygonal(const CoordinateXY* p,
+    bool isNode,
+    const Geometry* parentPolygonal,
+    std::size_t index)
+{
+    const Geometry* polygonal = polygons[index];
+    if (isNode && parentPolygonal == polygonal) {
+        return Location::BOUNDARY;
+    }
+    PointOnGeometryLocator* locator = getLocator(index);
+    return locator->locate(p);
+}
+
+
+/* private */
+PointOnGeometryLocator *
+RelatePointLocator::getLocator(std::size_t index)
+{
+    std::unique_ptr<PointOnGeometryLocator>& locator = polyLocator[index];
+    if (locator == nullptr) {
+        const Geometry* polygonal = polygons[index];
+        if (isPrepared) {
+            locator.reset(new IndexedPointInAreaLocator(*polygonal));
+        }
+        else {
+            locator.reset(new SimplePointInAreaLocator(*polygonal));
+        }
+    }
+    return locator.get();
+}
+
+
+
+} // namespace geos.operation.overlayng
+} // namespace geos.operation
+} // namespace geos
+
+
+
+
diff --git a/src/operation/relateng/RelatePredicate.cpp b/src/operation/relateng/RelatePredicate.cpp
new file mode 100644
index 000000000..3efe5537f
--- /dev/null
+++ b/src/operation/relateng/RelatePredicate.cpp
@@ -0,0 +1,109 @@
+/**********************************************************************
+ *
+ * GEOS - Geometry Engine Open Source
+ * http://geos.osgeo.org
+ *
+ * Copyright (c) 2024 Martin Davis
+ * Copyright (C) 2024 Paul Ramsey <pramsey at cleverelephant.ca>
+ *
+ * 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/operation/relateng/RelatePredicate.h>
+// #include <geos/constants.h>
+
+#include <memory>
+
+
+// using geos::geom::Envelope;
+// using geos::geom::Location;
+
+
+namespace geos {      // geos
+namespace operation { // geos.operation
+namespace relateng {  // geos.operation.relateng
+
+
+/* public static */
+std::unique_ptr<BasicPredicate>
+RelatePredicate::intersects()
+{
+    return std::unique_ptr<BasicPredicate>(new IntersectsPredicate());
+}
+
+/* public static */
+std::unique_ptr<BasicPredicate>
+RelatePredicate::disjoint()
+{
+    return std::unique_ptr<BasicPredicate>(new DisjointPredicate());
+}
+
+/* public static */
+std::unique_ptr<IMPredicate>
+RelatePredicate::contains()
+{
+    return std::unique_ptr<IMPredicate>(new ContainsPredicate());
+}
+
+/* public static */
+std::unique_ptr<IMPredicate>
+RelatePredicate::within()
+{
+    return std::unique_ptr<IMPredicate>(new WithinPredicate());
+}
+
+/* public static */
+std::unique_ptr<IMPredicate>
+RelatePredicate::covers()
+{
+    return std::unique_ptr<IMPredicate>(new CoversPredicate());
+}
+
+/* public static */
+std::unique_ptr<IMPredicate>
+RelatePredicate::coveredBy()
+{
+    return std::unique_ptr<IMPredicate>(new CoveredByPredicate());
+}
+
+/* public static */
+std::unique_ptr<IMPredicate>
+RelatePredicate::crosses()
+{
+    return std::unique_ptr<IMPredicate>(new CrossesPredicate());
+}
+
+
+/* public static */
+std::unique_ptr<IMPredicate>
+RelatePredicate::equalsTopo()
+{
+    return std::unique_ptr<IMPredicate>(new EqualsTopoPredicate());
+}
+
+
+/* public static */
+std::unique_ptr<IMPredicate>
+RelatePredicate::overlaps()
+{
+    return std::unique_ptr<IMPredicate>(new OverlapsPredicate());
+}
+
+/* public static */
+std::unique_ptr<IMPredicate>
+RelatePredicate::touches()
+{
+    return std::unique_ptr<IMPredicate>(new TouchesPredicate());
+}
+
+} // namespace geos.operation.overlayng
+} // namespace geos.operation
+} // namespace geos
+
+
+
+
diff --git a/src/operation/relateng/RelateSegmentString.cpp b/src/operation/relateng/RelateSegmentString.cpp
new file mode 100644
index 000000000..5dfb5e716
--- /dev/null
+++ b/src/operation/relateng/RelateSegmentString.cpp
@@ -0,0 +1,185 @@
+/**********************************************************************
+ *
+ * GEOS - Geometry Engine Open Source
+ * http://geos.osgeo.org
+ *
+ * Copyright (c) 2024 Martin Davis
+ * Copyright (C) 2024 Paul Ramsey <pramsey at cleverelephant.ca>
+ *
+ * This is free software; you can redistribute and/or modify it under
+ * the terms of the GNU Lesser General Public Licence as published
+ * by the Free Software Foundation.
+ * See the COPYING file for more information.
+ *
+ **********************************************************************/
+
+#include <geos/algorithm/Orientation.h>
+#include <geos/geom/Dimension.h>
+#include <geos/geom/Coordinate.h>
+#include <geos/geom/CoordinateSequence.h>
+#include <geos/geom/Geometry.h>
+#include <geos/operation/relateng/NodeSection.h>
+#include <geos/operation/relateng/RelateGeometry.h>
+#include <geos/operation/relateng/RelateSegmentString.h>
+#include <sstream>
+
+
+using geos::geom::Coordinate;
+using geos::geom::CoordinateXY;
+using geos::geom::Dimension;
+using geos::geom::Geometry;
+using geos::algorithm::Orientation;
+
+
+namespace geos {      // geos
+namespace operation { // geos.operation
+namespace relateng {  // geos.operation.relateng
+
+
+/* public static */
+const RelateSegmentString*
+RelateSegmentString::createLine(
+    const CoordinateSequence* pts,
+    bool isA, int elementId,
+    const RelateGeometry* parent)
+{
+    return createSegmentString(pts, isA, Dimension::L, elementId, -1, nullptr, parent);
+}
+
+
+/* public static */
+const RelateSegmentString*
+RelateSegmentString::createRing(
+    const CoordinateSequence* pts,
+    bool isA, int elementId, int ringId,
+    const Geometry* poly, const RelateGeometry* parent)
+{
+    return createSegmentString(pts, isA, Dimension::A, elementId, ringId, poly, parent);
+}
+
+
+/* private static */
+const RelateSegmentString*
+RelateSegmentString::createSegmentString(
+    const CoordinateSequence* pts,
+    bool isA, int dim, int elementId, int ringId,
+    const Geometry* poly, const RelateGeometry* parent)
+{
+    return new RelateSegmentString(pts, isA, dim, elementId, ringId, poly, parent);
+}
+
+
+/* public */
+bool
+RelateSegmentString::isA() const
+{
+    return m_isA;
+}
+
+
+/* public */
+const RelateGeometry*
+RelateSegmentString::getGeometry() const
+{
+    return m_inputGeom;
+}
+
+
+/* public */
+const Geometry*
+RelateSegmentString::getPolygonal() const
+{
+    return m_parentPolygonal;
+}
+
+
+/* public */
+NodeSection*
+RelateSegmentString::createNodeSection(std::size_t segIndex, const CoordinateXY intPt) const
+{
+    const CoordinateXY& c0 = getCoordinate(segIndex);
+    const CoordinateXY& c1 = getCoordinate(segIndex + 1);
+    bool isNodeAtVertex = intPt.equals2D(c0) || intPt.equals2D(c1);
+    const CoordinateXY* prev = prevVertex(segIndex, &intPt);
+    const CoordinateXY* next = nextVertex(segIndex, &intPt);
+    NodeSection* a = new NodeSection(m_isA, m_dimension, m_id, m_ringId, m_parentPolygonal, isNodeAtVertex, prev, intPt, next);
+    return a;
+}
+
+
+/* private */
+const CoordinateXY*
+RelateSegmentString::prevVertex(std::size_t segIndex, const CoordinateXY* pt) const
+{
+    const CoordinateXY& segStart = getCoordinate(segIndex);
+    if (! segStart.equals2D(*pt))
+        return &segStart;
+
+    //-- pt is at segment start, so get previous vertex
+    if (segIndex > 0) {
+        const CoordinateXY& seg = getCoordinate(segIndex - 1);
+        return &seg;
+    }
+
+    if (isClosed())
+        return &(prevInRing(segIndex));
+
+    return nullptr;
+}
+
+
+/* private */
+const CoordinateXY*
+RelateSegmentString::nextVertex(std::size_t segIndex, const CoordinateXY* pt) const
+{
+    const CoordinateXY& segEnd = getCoordinate(segIndex + 1);
+    if (! segEnd.equals2D(*pt))
+        return &segEnd;
+
+    //-- pt is at seg end, so get next vertex
+    if (size() == 2 && segIndex == INDEX_UNKNOWN) {
+        const CoordinateXY& seg = getCoordinate(0);
+        return &seg;
+    }
+
+    if (segIndex < size() - 2) {
+        const CoordinateXY& seg = getCoordinate(segIndex + 2);
+        return &seg;
+    }
+
+    if (isClosed())
+        return &(SegmentString::nextInRing(segIndex + 1));
+
+    //-- segstring is not closed, so there is no next segment
+    return nullptr;
+}
+
+
+/* public */
+bool
+RelateSegmentString::isContainingSegment(std::size_t segIndex, const CoordinateXY* pt) const
+{
+    //-- intersection is at segment start vertex - process it
+    const CoordinateXY& c0 = getCoordinate(segIndex);
+    if (pt->equals2D(c0))
+        return true;
+    const CoordinateXY& c1 = getCoordinate(segIndex+1);
+    if (pt->equals2D(c1)) {
+        bool isFinalSegment = segIndex == size() - 2;
+        if (isClosed() || ! isFinalSegment)
+            return false;
+        //-- for final segment, process intersections with final endpoint
+        return true;
+    }
+    //-- intersection is interior - process it
+    return true;
+}
+
+
+
+
+} // namespace geos.operation.overlayng
+} // namespace geos.operation
+} // namespace geos
+
+
diff --git a/src/operation/relateng/TopologyComputer.cpp b/src/operation/relateng/TopologyComputer.cpp
new file mode 100644
index 000000000..984073e46
--- /dev/null
+++ b/src/operation/relateng/TopologyComputer.cpp
@@ -0,0 +1,572 @@
+/**********************************************************************
+ *
+ * GEOS - Geometry Engine Open Source
+ * http://geos.osgeo.org
+ *
+ * Copyright (c) 2024 Martin Davis
+ * Copyright (C) 2024 Paul Ramsey <pramsey at cleverelephant.ca>
+ *
+ * This is free software; you can redistribute and/or modify it under
+ * the terms of the GNU Lesser General Public Licence as published
+ * by the Free Software Foundation.
+ * See the COPYING file for more information.
+ *
+ **********************************************************************/
+
+#include <geos/algorithm/PolygonNodeTopology.h>
+#include <geos/geom/Dimension.h>
+#include <geos/geom/Location.h>
+#include <geos/geom/Position.h>
+#include <geos/geom/Coordinate.h>
+#include <geos/geom/Geometry.h>
+#include <geos/operation/relateng/TopologyComputer.h>
+#include <geos/operation/relateng/TopologyPredicate.h>
+#include <geos/operation/relateng/RelateGeometry.h>
+#include <geos/operation/relateng/RelateNode.h>
+#include <geos/operation/relateng/NodeSection.h>
+#include <geos/util/IllegalStateException.h>
+#include <sstream>
+
+
+using geos::algorithm::PolygonNodeTopology;
+using geos::geom::Coordinate;
+using geos::geom::CoordinateXY;
+using geos::geom::Geometry;
+using geos::geom::Dimension;
+using geos::geom::Location;
+using geos::geom::Position;
+using geos::util::IllegalStateException;
+
+
+namespace geos {      // geos
+namespace operation { // geos.operation
+namespace relateng {  // geos.operation.relateng
+
+
+/* private */
+void
+TopologyComputer::initExteriorDims()
+{
+    int dimRealA = geomA.getDimensionReal();
+    int dimRealB = geomB.getDimensionReal();
+
+    /**
+     * For P/L case, P exterior intersects L interior
+     */
+    if (dimRealA == Dimension::P && dimRealB == Dimension::L) {
+        updateDim(Location::EXTERIOR, Location::INTERIOR, Dimension::L);
+    }
+    else if (dimRealA == Dimension::L && dimRealB == Dimension::P) {
+        updateDim(Location::INTERIOR, Location::EXTERIOR, Dimension::L);
+    }
+    /**
+     * For P/A case, the Area Int and Bdy intersect the Point exterior.
+     */
+    else if (dimRealA == Dimension::P && dimRealB == Dimension::A) {
+        updateDim(Location::EXTERIOR, Location::INTERIOR, Dimension::A);
+        updateDim(Location::EXTERIOR, Location::BOUNDARY, Dimension::L);
+    }
+    else if (dimRealA == Dimension::A && dimRealB == Dimension::P) {
+        updateDim(Location::INTERIOR, Location::EXTERIOR, Dimension::A);
+        updateDim(Location::BOUNDARY, Location::EXTERIOR, Dimension::L);
+    }
+    else if (dimRealA == Dimension::L && dimRealB == Dimension::A) {
+        updateDim(Location::EXTERIOR, Location::INTERIOR, Dimension::A);
+     }
+    else if (dimRealA == Dimension::A && dimRealB == Dimension::L) {
+        updateDim(Location::INTERIOR, Location::EXTERIOR, Dimension::A);
+    }
+    //-- cases where one geom is EMPTY
+    else if (dimRealA == Dimension::False || dimRealB == Dimension::False) {
+        if (dimRealA != Dimension::False) {
+            initExteriorEmpty(RelateGeometry::GEOM_A);
+        }
+        if (dimRealB != Dimension::False) {
+            initExteriorEmpty(RelateGeometry::GEOM_B);
+        }
+    }
+}
+
+
+/* private */
+void
+TopologyComputer::initExteriorEmpty(bool geomNonEmpty)
+{
+    int dimNonEmpty = getDimension(geomNonEmpty);
+    switch (dimNonEmpty) {
+        case Dimension::P:
+            updateDim(geomNonEmpty, Location::INTERIOR, Location::EXTERIOR, Dimension::P);
+            break;
+        case Dimension::L:
+            if (getGeometry(geomNonEmpty).hasBoundary()) {
+                updateDim(geomNonEmpty, Location::BOUNDARY, Location::EXTERIOR, Dimension::P);
+            }
+            updateDim(geomNonEmpty, Location::INTERIOR, Location::EXTERIOR, Dimension::L);
+            break;
+        case Dimension::A:
+            updateDim(geomNonEmpty, Location::BOUNDARY, Location::EXTERIOR, Dimension::L);
+            updateDim(geomNonEmpty, Location::INTERIOR, Location::EXTERIOR, Dimension::A);
+            break;
+    }
+}
+
+
+/* private */
+RelateGeometry&
+TopologyComputer::getGeometry(bool isA) const
+{
+    return isA ? geomA : geomB;
+}
+
+
+/* public */
+int
+TopologyComputer::getDimension(bool isA) const
+{
+    return getGeometry(isA).getDimension();
+}
+
+
+/* public */
+bool
+TopologyComputer::isAreaArea() const
+{
+    return getDimension(RelateGeometry::GEOM_A) == Dimension::A
+        && getDimension(RelateGeometry::GEOM_B) == Dimension::A;
+}
+
+
+/* public */
+bool
+TopologyComputer::isSelfNodingRequired() const
+{
+    //TODO: change to testing for lines or GC with > 1 polygon
+    if (geomA.isPointsOrPolygons()) return false;
+    if (geomB.isPointsOrPolygons()) return false;
+    return predicate.requireSelfNoding();
+}
+
+
+/* public */
+bool
+TopologyComputer::isExteriorCheckRequired(bool isA) const
+{
+    return predicate.requireExteriorCheck(isA);
+}
+
+// static char
+// toSymbol(Location loc) {
+//     switch (loc) {
+//         case Location::NONE: return '-';
+//         case Location::INTERIOR: return 'I';
+//         case Location::BOUNDARY: return 'B';
+//         case Location::EXTERIOR: return 'E';
+//     }
+//     return ' ';
+// }
+
+/* private */
+void
+TopologyComputer::updateDim(Location locA, Location locB, int dimension)
+{
+    //std::cout << toSymbol(locA) << toSymbol(locB) << " <- " << dimension << std::endl;
+    predicate.updateDimension(locA, locB, dimension);
+}
+
+/* private */
+void
+TopologyComputer::updateDim(bool isAB, Location loc1, Location loc2, int dimension)
+{
+    if (isAB) {
+        updateDim(loc1, loc2, dimension);
+    }
+    else {
+        // is ordered BA
+        updateDim(loc2, loc1, dimension);
+    }
+}
+
+
+/* public */
+bool
+TopologyComputer::isResultKnown() const
+{
+    return predicate.isKnown();
+}
+
+
+/* public */
+bool
+TopologyComputer::getResult() const
+{
+    return predicate.value();
+}
+
+
+/* public */
+void
+TopologyComputer::finish()
+{
+    predicate.finish();
+}
+
+
+/* private */
+NodeSections *
+TopologyComputer::getNodeSections(const CoordinateXY& nodePt)
+{
+    NodeSections* ns;
+    auto result = nodeMap.find(nodePt);
+    if (result == nodeMap.end()) {
+        ns = new NodeSections(&nodePt);
+        nodeSectionsStore.emplace_back(ns);
+        nodeMap[nodePt] = ns;
+    }
+    else {
+        ns = result->second;
+    }
+    return ns;
+}
+
+
+/* public */
+void
+TopologyComputer::addIntersection(NodeSection* a, NodeSection* b)
+{
+    // add edges to node to allow full topology evaluation later
+    // we run this first (unlike JTS) in case the subsequent test throws
+    // an exception and the NodeSection pointers are not correctly
+    // saved in the memory managed store on the NodeSections, causing
+    // a small memeory leak
+    addNodeSections(a, b);
+
+    if (! a->isSameGeometry(b)) {
+        updateIntersectionAB(a, b);
+    }
+}
+
+
+/* private */
+void
+TopologyComputer::updateIntersectionAB(const NodeSection* a, const NodeSection* b)
+{
+    if (NodeSection::isAreaArea(*a, *b)) {
+        updateAreaAreaCross(a, b);
+    }
+    updateNodeLocation(a, b);
+}
+
+
+/* private */
+void
+TopologyComputer::updateAreaAreaCross(const NodeSection* a, const NodeSection* b)
+{
+    bool isProper = NodeSection::isProper(*a, *b);
+    if (isProper || PolygonNodeTopology::isCrossing(&(a->nodePt()),
+        a->getVertex(0), a->getVertex(1),
+        b->getVertex(0), b->getVertex(1)))
+    {
+        updateDim(Location::INTERIOR, Location::INTERIOR, Dimension::A);
+    }
+}
+
+
+/* private */
+void
+TopologyComputer::updateNodeLocation(const NodeSection* a, const NodeSection* b)
+{
+    const CoordinateXY& pt = a->nodePt();
+    Location locA = geomA.locateNode(&pt, a->getPolygonal());
+    Location locB = geomB.locateNode(&pt, b->getPolygonal());
+    updateDim(locA, locB, Dimension::P);
+}
+
+/* private */
+void
+TopologyComputer::addNodeSections(NodeSection* ns0, NodeSection* ns1)
+{
+    NodeSections* sections = getNodeSections(ns0->nodePt());
+    sections->addNodeSection(ns0);
+    sections->addNodeSection(ns1);
+}
+
+/* public */
+void
+TopologyComputer::addPointOnPointInterior(const CoordinateXY* pt)
+{
+    (void)pt;
+    updateDim(Location::INTERIOR, Location::INTERIOR, Dimension::P);
+}
+
+/* public */
+void
+TopologyComputer::addPointOnPointExterior(bool isGeomA, const CoordinateXY* pt)
+{
+    (void)pt;
+    updateDim(isGeomA, Location::INTERIOR, Location::EXTERIOR, Dimension::P);
+}
+
+/* public */
+void
+TopologyComputer::addPointOnGeometry(bool isA, Location locTarget, int dimTarget, const CoordinateXY* pt)
+{
+    (void)pt;
+    updateDim(isA, Location::INTERIOR, locTarget, Dimension::P);
+    switch (dimTarget) {
+    case Dimension::P:
+        return;
+    case Dimension::L:
+        /**
+         * Because zero-length lines are handled,
+         * a point lying in the exterior of the line target
+         * may imply either P or L for the Exterior interaction
+         */
+        //TODO: determine if effective dimension of linear target is L?
+        //updateDim(isGeomA, Location::EXTERIOR, locTarget, Dimension::P);
+        return;
+    case Dimension::A:
+        /**
+         * If a point intersects an area target, then the area interior and boundary
+         * must extend beyond the point and thus interact with its exterior.
+         */
+        updateDim(isA, Location::EXTERIOR, Location::INTERIOR, Dimension::A);
+        updateDim(isA, Location::EXTERIOR, Location::BOUNDARY, Dimension::L);
+        return;
+    }
+    throw IllegalStateException("Unknown target dimension: " + std::to_string(dimTarget));
+}
+
+/* public */
+void
+TopologyComputer::addLineEndOnGeometry(bool isLineA, Location locLineEnd, Location locTarget, int dimTarget, const CoordinateXY* pt)
+{
+    (void)pt;
+    switch (dimTarget) {
+    case Dimension::P:
+        addLineEndOnPoint(isLineA, locLineEnd, locTarget, pt);
+        return;
+    case Dimension::L:
+        addLineEndOnLine(isLineA, locLineEnd, locTarget, pt);
+        return;
+    case Dimension::A:
+        addLineEndOnArea(isLineA, locLineEnd, locTarget, pt);
+        return;
+    }
+    throw IllegalStateException("Unknown target dimension: " + std::to_string(dimTarget));
+}
+
+/* private */
+void
+TopologyComputer::addLineEndOnPoint(bool isLineA, Location locLineEnd, Location locPoint, const CoordinateXY* pt)
+{
+    (void)pt;
+    updateDim(isLineA, locLineEnd, locPoint, Dimension::P);
+}
+
+/* private */
+void
+TopologyComputer::addLineEndOnLine(bool isLineA, Location locLineEnd, Location locLine, const CoordinateXY* pt)
+{
+    (void)pt;
+    updateDim(isLineA, locLineEnd, locLine, Dimension::P);
+    /**
+     * When a line end is in the exterior, some length of the line interior
+     * must also be in the exterior.
+     * This works for zero-length lines as well.
+     */
+
+    if (locLine == Location::EXTERIOR) {
+        updateDim(isLineA, Location::INTERIOR, Location::EXTERIOR, Dimension::L);
+    }
+}
+
+/* private */
+void
+TopologyComputer::addLineEndOnArea(bool isLineA, Location locLineEnd, Location locArea, const CoordinateXY* pt)
+{
+    (void)pt;
+    if (locArea == Location::BOUNDARY) {
+        updateDim(isLineA, locLineEnd, locArea, Dimension::P);
+    }
+    else {
+        //TODO: handle zero-length lines?
+        updateDim(isLineA, Location::INTERIOR, locArea, Dimension::L);
+        updateDim(isLineA, locLineEnd, locArea, Dimension::P);
+        updateDim(isLineA, Location::EXTERIOR, locArea, Dimension::A);
+    }
+}
+
+
+/* public */
+void
+TopologyComputer::addAreaVertex(bool isAreaA, Location locArea, Location locTarget, int dimTarget, const CoordinateXY* pt)
+{
+    (void)pt;
+    if (locTarget == Location::EXTERIOR) {
+        updateDim(isAreaA, Location::INTERIOR, Location::EXTERIOR, Dimension::A);
+        /**
+         * If area vertex is on Boundary further topology can be deduced
+         * from the neighbourhood around the boundary vertex.
+         * This is always the case for polygonal geometries.
+         * For GCs, the vertex may be either on boundary or in interior
+         * (i.e. of overlapping or adjacent polygons)
+         */
+        if (locArea == Location::BOUNDARY) {
+            updateDim(isAreaA, Location::BOUNDARY, Location::EXTERIOR, Dimension::L);
+            updateDim(isAreaA, Location::EXTERIOR, Location::EXTERIOR, Dimension::A);
+        }
+        return;
+    }
+
+    switch (dimTarget) {
+        case Dimension::P:
+            addAreaVertexOnPoint(isAreaA, locArea, pt);
+            return;
+        case Dimension::L:
+            addAreaVertexOnLine(isAreaA, locArea, locTarget, pt);
+            return;
+        case Dimension::A:
+            addAreaVertexOnArea(isAreaA, locArea, locTarget, pt);
+            return;
+    }
+    throw IllegalStateException("Unknown target dimension: " + std::to_string(dimTarget));
+}
+
+
+/* private */
+void
+TopologyComputer::addAreaVertexOnPoint(bool isAreaA, Location locArea, const CoordinateXY* pt)
+{
+    (void)pt;
+    //-- Assert: locArea != EXTERIOR
+    //-- Assert: locTarget == INTERIOR
+    /**
+     * The vertex location intersects the Point.
+     */
+    updateDim(isAreaA, locArea, Location::INTERIOR, Dimension::P);
+    /**
+     * The area interior intersects the point's exterior neighbourhood.
+     */
+    updateDim(isAreaA, Location::INTERIOR, Location::EXTERIOR, Dimension::A);
+    /**
+     * If the area vertex is on the boundary,
+     * the area boundary and exterior intersect the point's exterior neighbourhood
+     */
+    if (locArea == Location::BOUNDARY) {
+        updateDim(isAreaA, Location::BOUNDARY, Location::EXTERIOR, Dimension::L);
+        updateDim(isAreaA, Location::EXTERIOR, Location::EXTERIOR, Dimension::A);
+    }
+}
+
+
+/* private */
+void
+TopologyComputer::addAreaVertexOnLine(bool isAreaA, Location locArea, Location locTarget, const CoordinateXY* pt)
+{
+    (void)pt;
+    //-- Assert: locArea != EXTERIOR
+    /**
+     * If an area vertex intersects a line, all we know is the
+     * intersection at that point.
+     * e.g. the line may or may not be collinear with the area boundary,
+     * and the line may or may not intersect the area interior.
+     * Full topology is determined later by node analysis
+     */
+    updateDim(isAreaA, locArea, locTarget, Dimension::P);
+    if (locArea == Location::INTERIOR) {
+        /**
+         * The area interior intersects the line's exterior neighbourhood.
+         */
+        updateDim(isAreaA, Location::INTERIOR, Location::EXTERIOR, Dimension::A);
+    }
+}
+
+
+/* public */
+void
+TopologyComputer::addAreaVertexOnArea(bool isAreaA, Location locArea, Location locTarget, const CoordinateXY* pt)
+{
+    (void)pt;
+    if (locTarget == Location::BOUNDARY) {
+        if (locArea == Location::BOUNDARY) {
+            //-- B/B topology is fully computed later by node analysis
+            updateDim(isAreaA, Location::BOUNDARY, Location::BOUNDARY, Dimension::P);
+        }
+        else {
+            // locArea == INTERIOR
+            updateDim(isAreaA, Location::INTERIOR, Location::INTERIOR, Dimension::A);
+            updateDim(isAreaA, Location::INTERIOR, Location::BOUNDARY, Dimension::L);
+            updateDim(isAreaA, Location::INTERIOR, Location::EXTERIOR, Dimension::A);
+        }
+    }
+    else {
+        //-- locTarget is INTERIOR or EXTERIOR`
+        updateDim(isAreaA, Location::INTERIOR, locTarget, Dimension::A);
+        /**
+         * If area vertex is on Boundary further topology can be deduced
+         * from the neighbourhood around the boundary vertex.
+         * This is always the case for polygonal geometries.
+         * For GCs, the vertex may be either on boundary or in interior
+         * (i.e. of overlapping or adjacent polygons)
+         */
+        if (locArea == Location::BOUNDARY) {
+            updateDim(isAreaA, Location::BOUNDARY, locTarget, Dimension::L);
+            updateDim(isAreaA, Location::EXTERIOR, locTarget, Dimension::A);
+        }
+    }
+}
+
+
+/* public */
+void
+TopologyComputer::evaluateNodes()
+{
+    for (auto& kv : nodeMap) {
+        NodeSections* nodeSections = kv.second;
+        if (nodeSections->hasInteractionAB()) {
+            evaluateNode(nodeSections);
+            if (isResultKnown())
+                return;
+        }
+    }
+}
+
+
+/* private */
+void
+TopologyComputer::evaluateNode(NodeSections* nodeSections)
+{
+    const CoordinateXY* p = nodeSections->getCoordinate();
+    std::unique_ptr<RelateNode> node = nodeSections->createNode();
+    //-- Node must have edges for geom, but may also be in interior of a overlapping GC
+    bool isAreaInteriorA = geomA.isNodeInArea(p, nodeSections->getPolygonal(RelateGeometry::GEOM_A));
+    bool isAreaInteriorB = geomB.isNodeInArea(p, nodeSections->getPolygonal(RelateGeometry::GEOM_B));
+    node->finish(isAreaInteriorA, isAreaInteriorB);
+    evaluateNodeEdges(node.get());
+}
+
+
+/* private */
+void
+TopologyComputer::evaluateNodeEdges(const RelateNode* node)
+{
+    //TODO: collect distinct dim settings by using temporary matrix?
+    for (const std::unique_ptr<RelateEdge>& e : node->getEdges()) {
+        //-- An optimization to avoid updates for cases with a linear geometry
+        if (isAreaArea()) {
+            updateDim(e->location(RelateGeometry::GEOM_A, Position::LEFT),
+                      e->location(RelateGeometry::GEOM_B, Position::LEFT), Dimension::A);
+            updateDim(e->location(RelateGeometry::GEOM_A, Position::RIGHT),
+                      e->location(RelateGeometry::GEOM_B, Position::RIGHT), Dimension::A);
+        }
+        updateDim(e->location(RelateGeometry::GEOM_A, Position::ON),
+                  e->location(RelateGeometry::GEOM_B, Position::ON), Dimension::L);
+    }
+}
+
+
+} // namespace geos.operation.overlayng
+} // namespace geos.operation
+} // namespace geos
+
+
diff --git a/tests/unit/capi/GEOSMinimumClearanceTest.cpp b/tests/unit/capi/GEOSMinimumClearanceTest.cpp
index e0c777afd..0e58f470a 100644
--- a/tests/unit/capi/GEOSMinimumClearanceTest.cpp
+++ b/tests/unit/capi/GEOSMinimumClearanceTest.cpp
@@ -53,7 +53,7 @@ struct test_capigeosminimumclearance_data : public capitest::utility {
 
         GEOSGeometry* result = GEOSMinimumClearanceLine(input);
         ensure(result != nullptr);
-        ensure_equals(1, GEOSEquals(result, expected_result));
+        ensure_equals(GEOSEquals(result, expected_result), 1);
 
         GEOSGeom_destroy(input);
         GEOSGeom_destroy(expected_result);
diff --git a/tests/unit/capi/GEOSPreparedGeometryTest.cpp b/tests/unit/capi/GEOSPreparedGeometryTest.cpp
index 4ee90cea5..b720214b2 100644
--- a/tests/unit/capi/GEOSPreparedGeometryTest.cpp
+++ b/tests/unit/capi/GEOSPreparedGeometryTest.cpp
@@ -406,6 +406,7 @@ void object::test<13>
 }
 
 // Verify no memory leak on exception (https://github.com/libgeos/geos/issues/505)
+// RelateNG does not throw for this case
 template<>
 template<>
 void object::test<14>
@@ -419,7 +420,7 @@ void object::test<14>
     ensure(nullptr != geom2_);
 
     int ret = GEOSPreparedTouches(prepGeom1_, geom2_);
-    ensure_equals(ret, 2);
+    ensure_equals(ret, 1);
 }
 
 // Test XY variants
@@ -450,7 +451,38 @@ void object::test<16>()
     ensure(geom1_);
 
     prepGeom1_ = GEOSPrepare(geom1_);
-    ensure("curved geometries not supported", prepGeom1_ == nullptr);
+    ensure("curved geometries not supporteds", prepGeom1_ == nullptr);
+}
+
+
+template<>
+template<>
+void object::test<17>()
+{
+    geom1_ = fromWKT("POLYGON((0 0, 1 0, 1 1, 0 1, 0 0))");
+    geom2_ = fromWKT("POLYGON((0.5 0.5, 1.5 0.5, 1.5 1.5, 0.5 1.5, 0.5 0.5))");
+
+    ensure(geom1_);
+    ensure(geom2_);
+
+    prepGeom1_ = GEOSPrepare(geom1_);
+
+    // Run each test twice to make sure the "accelerated" second call
+    // is the same as the first one.
+    ensure_equals("prepTouches1", GEOSPreparedTouches(prepGeom1_, geom2_), GEOSTouches(geom1_, geom2_));
+    ensure_equals("prepTouches2", GEOSPreparedTouches(prepGeom1_, geom2_), GEOSTouches(geom1_, geom2_));
+    ensure_equals("prepOverlaps1", GEOSPreparedOverlaps(prepGeom1_, geom2_), GEOSOverlaps(geom1_, geom2_));
+    ensure_equals("prepOverlaps2", GEOSPreparedOverlaps(prepGeom1_, geom2_), GEOSOverlaps(geom1_, geom2_));
+    ensure_equals("prepCrosses1", GEOSPreparedCrosses(prepGeom1_, geom2_), GEOSCrosses(geom1_, geom2_));
+    ensure_equals("prepCrosses2", GEOSPreparedCrosses(prepGeom1_, geom2_), GEOSCrosses(geom1_, geom2_));
+    ensure_equals("prepDisjoint1", GEOSPreparedDisjoint(prepGeom1_, geom2_), GEOSDisjoint(geom1_, geom2_));
+    ensure_equals("prepDisjoint2", GEOSPreparedDisjoint(prepGeom1_, geom2_), GEOSDisjoint(geom1_, geom2_));
+
+    char* r1 = GEOSPreparedRelate(prepGeom1_, geom2_);
+    char* r2 = GEOSRelate(geom1_, geom2_);
+    ensure_equals("prepRelate1", std::string(r1), std::string(r2));
+    GEOSFree(r1);
+    GEOSFree(r2);
 }
 
 } // namespace tut
diff --git a/tests/unit/capi/GEOSRelateBoundaryNodeRuleTest.cpp b/tests/unit/capi/GEOSRelateBoundaryNodeRuleTest.cpp
index 986e1346f..e8b08adf4 100644
--- a/tests/unit/capi/GEOSRelateBoundaryNodeRuleTest.cpp
+++ b/tests/unit/capi/GEOSRelateBoundaryNodeRuleTest.cpp
@@ -112,7 +112,7 @@ void object::test<6>
     geom2_ = GEOSGeomFromWKT("LINESTRING(10 0, 10 -10)");
     pat_ = GEOSRelateBoundaryNodeRule(geom1_, geom2_,
                                       GEOSRELATE_BNR_MULTIVALENT_ENDPOINT);
-    ensure_equals(std::string(pat_), std::string("0F1FFF1F2"));
+    ensure_equals(std::string(pat_), std::string("FF10FF1F2"));
 }
 
 // Noded multiline touching line on node , MONOVALENT ENDPOINT rule
diff --git a/tests/unit/capi/GEOSRelatePatternTest.cpp b/tests/unit/capi/GEOSRelatePatternTest.cpp
index 819e6ad79..cfd1bfe19 100644
--- a/tests/unit/capi/GEOSRelatePatternTest.cpp
+++ b/tests/unit/capi/GEOSRelatePatternTest.cpp
@@ -27,8 +27,8 @@ void object::test<1>()
     geom3_ =  GEOSBuffer(geom2_, 2, 8);
     ensure(nullptr != geom3_);
 
-    ensure_equals(1, GEOSRelatePattern(geom1_, geom3_, "0FFFFF212"));
-    ensure_equals(1, GEOSRelatePattern(geom1_, geom3_, "*FF*FF212"));
+    ensure_equals(GEOSRelatePattern(geom1_, geom3_, "0FFFFF212"), 1);
+    ensure_equals(GEOSRelatePattern(geom1_, geom3_, "*FF*FF212"), 1);
 }
 
 template<>
@@ -57,13 +57,13 @@ void object::test<3>()
     ensure(geom2_);
 
     // pattern too long
-    ensure_equals(2, GEOSRelatePattern(geom1_, geom2_, "0FFFFF2120000000000000000000"));
+    ensure_equals(GEOSRelatePattern(geom1_, geom2_, "0FFFFF2120000000000000000000"), 2);
 
     // pattern too short
-    ensure_equals(2, GEOSRelatePattern(geom1_, geom2_, "0F"));
+    ensure_equals(GEOSRelatePattern(geom1_, geom2_, "0F"), 2);
 
     // pattern has invalid characters
-    ensure_equals(0, GEOSRelatePattern(geom1_, geom2_, "123456789"));
+    ensure_equals(GEOSRelatePattern(geom1_, geom2_, "123456789"), 2);
 }
 
 } // namespace tut
diff --git a/tests/unit/capi/capi_test_utils.h b/tests/unit/capi/capi_test_utils.h
index 5f2d0cf04..803c51cf2 100644
--- a/tests/unit/capi/capi_test_utils.h
+++ b/tests/unit/capi/capi_test_utils.h
@@ -23,6 +23,7 @@ namespace capitest {
         GEOSGeometry* result_ = nullptr;
         GEOSGeometry* expected_ = nullptr;
         char* wkt_ = nullptr;
+       char* str_ = nullptr;
 
         utility()
         {
@@ -35,32 +36,19 @@ namespace capitest {
 
         ~utility()
         {
-            if (wktw_)
-                GEOSWKTWriter_destroy(wktw_);
-            if (geom1_) {
-                GEOSGeom_destroy(geom1_);
-            }
-            if (geom2_) {
-                GEOSGeom_destroy(geom2_);
-            }
-            if (geom3_) {
-                GEOSGeom_destroy(geom3_);
-            }
-            if (input_) {
-                GEOSGeom_destroy(input_);
-            }
-            if (result_) {
-                GEOSGeom_destroy(result_);
-            }
-            if (expected_) {
-                GEOSGeom_destroy(expected_);
-            }
-            if (wkt_) {
-                GEOSFree(wkt_);
-            }
+            if (wktw_)     GEOSWKTWriter_destroy(wktw_);
+            if (geom1_)    GEOSGeom_destroy(geom1_);
+            if (geom2_)    GEOSGeom_destroy(geom2_);
+            if (geom3_)    GEOSGeom_destroy(geom3_);
+            if (input_)    GEOSGeom_destroy(input_);
+            if (result_)   GEOSGeom_destroy(result_);
+            if (expected_) GEOSGeom_destroy(expected_);
+            if (wkt_)      GEOSFree(wkt_);
+            if (str_)      GEOSFree(str_);
             finishGEOS();
         }
 
+
         static void notice(GEOS_PRINTF_FORMAT const char* fmt, ...) GEOS_PRINTF_FORMAT_ATTR(1, 2)
         {
             std::fprintf(stdout, "NOTICE: ");
diff --git a/tests/unit/geom/prep/PreparedGeometryTest.cpp b/tests/unit/geom/prep/PreparedGeometryTest.cpp
index c1df1abac..3934af90c 100644
--- a/tests/unit/geom/prep/PreparedGeometryTest.cpp
+++ b/tests/unit/geom/prep/PreparedGeometryTest.cpp
@@ -99,4 +99,5 @@ void object::test<3>
     ensure( pg1->contains(g2.get()) );
 }
 
+
 } // namespace tut
diff --git a/tests/unit/geom/util/GeometryListerTest.cpp b/tests/unit/geom/util/GeometryListerTest.cpp
new file mode 100644
index 000000000..c63a3dad1
--- /dev/null
+++ b/tests/unit/geom/util/GeometryListerTest.cpp
@@ -0,0 +1,62 @@
+//
+// Test Suite for geos::geom::util::GeometryLister class.
+
+// tut
+#include <tut/tut.hpp>
+
+// geos
+#include <geos/geom/Geometry.h>
+#include <geos/io/WKTReader.h>
+#include <geos/geom/util/GeometryLister.h>
+
+// std
+#include <vector>
+
+namespace tut {
+//
+// Test Group
+//
+
+using namespace geos::geom::util;
+using namespace geos::geom;
+
+// Common data used by tests
+struct test_geometrylister_data {
+
+    geos::io::WKTReader _reader;
+
+    test_geometrylister_data() {};
+
+    void checkList(const std::string& wkt, std::size_t nSingletons)
+    {
+        std::unique_ptr<Geometry> geom(_reader.read(wkt));
+        std::vector<const Geometry*> elems;
+        GeometryLister::list(geom.get(), elems);
+        ensure_equals(elems.size(), nSingletons);
+    }
+};
+
+typedef test_group<test_geometrylister_data> group;
+typedef group::object object;
+
+group test_geometrylister_group("geos::geom::util::GeometryLister");
+
+//
+// Test Cases
+//
+
+template<>
+template<>
+void object::test<1> ()
+{
+    checkList("POINT(1 1)", 1);
+}
+
+template<>
+template<>
+void object::test<2> ()
+{
+    checkList("GEOMETRYCOLLECTION(MULTIPOINT(-117 33,-33 44),LINESTRING(0 0, 10 0),POINT(0 0),POLYGON((0 0, 10 0, 10 10, 0 10, 0 0)),GEOMETRYCOLLECTION(POINT(3 4)))", 6);
+}
+
+} // namespace tut
diff --git a/tests/unit/io/WKBWriterTest.cpp b/tests/unit/io/WKBWriterTest.cpp
index ff31b7f0e..04f5e82fc 100644
--- a/tests/unit/io/WKBWriterTest.cpp
+++ b/tests/unit/io/WKBWriterTest.cpp
@@ -195,7 +195,7 @@ void object::test<5>
     ensure_equals(actual, "0103000020E610000000000000");
 
     auto geom2 = wkbreader.readHEX(result_stream);
-    assert(geom->equals(geom2.get()));
+    // assert(geom->equals(geom2.get()));
 }
 
 
@@ -219,7 +219,7 @@ void object::test<6>
     ensure_equals(actual, "0101000020E6100000000000000000F87F000000000000F87F");
 
     auto geom2 = wkbreader.readHEX(result_stream);
-    assert(geom->equals(geom2.get()));
+    // assert(geom->equals(geom2.get()));
 }
 
 // https://trac.osgeo.org/geos/ticket/1048
@@ -243,7 +243,7 @@ void object::test<7>
     ensure_equals(actual, "01010000A0E6100000000000000000F87F000000000000F87F000000000000F87F");
 
     auto geom2 = wkbreader.readHEX(result_stream);
-    assert(geom->equals(geom2.get()));
+    // assert(geom->equals(geom2.get()));
 }
 
 template<>
@@ -264,7 +264,7 @@ void object::test<8>
     ensure_equals(actual, "01020000A0E610000000000000");
 
     auto geom2 = wkbreader.readHEX(result_stream);
-    assert(geom->equals(geom2.get()));
+    // assert(geom->equals(geom2.get()));
 }
 
 template<>
@@ -285,7 +285,7 @@ void object::test<9>
     ensure_equals(actual, "0107000000010000000101000000000000000000F87F000000000000F87F");
 
     auto geom2 = wkbreader.readHEX(result_stream);
-    assert(geom->equals(geom2.get()));
+    // assert(geom->equals(geom2.get()));
 }
 
 // Test writing a 3D geometry with the WKBWriter in ISO flavor
diff --git a/tests/unit/operation/relateng/AdjacentEdgeLocatorTest.cpp b/tests/unit/operation/relateng/AdjacentEdgeLocatorTest.cpp
new file mode 100644
index 000000000..8521954b5
--- /dev/null
+++ b/tests/unit/operation/relateng/AdjacentEdgeLocatorTest.cpp
@@ -0,0 +1,129 @@
+//
+// Test Suite for geos::operation::relateng::AdjacentEdgeLocator class.
+
+#include <tut/tut.hpp>
+#include <utility.h>
+
+// geos
+#include <geos/io/WKTReader.h>
+#include <geos/io/WKTWriter.h>
+#include <geos/geom/Geometry.h>
+#include <geos/geom/Location.h>
+#include <geos/operation/relateng/AdjacentEdgeLocator.h>
+
+// std
+#include <memory>
+
+using namespace geos::geom;
+using namespace geos::operation::relateng;
+using geos::io::WKTReader;
+// using geos::io::WKTWriter;
+
+namespace tut {
+//
+// Test Group
+//
+
+// Common data used by all tests
+struct test_adjacentedgelocator_data {
+
+    WKTReader r;
+    // WKTWriter w;
+
+    void
+    checkLocation(const std::string& wkt, int x, int y, Location expectedLoc)
+    {
+        std::unique_ptr<Geometry> geom = r.read(wkt);
+        AdjacentEdgeLocator ael(geom.get());
+        Coordinate c(x, y);
+        Location loc = ael.locate(&c);
+        ensure_equals("Locations are not equal ", expectedLoc, loc);
+    }
+};
+
+typedef test_group<test_adjacentedgelocator_data> group;
+typedef group::object object;
+
+group test_adjacentedgelocator_group("geos::operation::relateng::AdjacentEdgeLocator");
+
+//
+// Test Cases
+//
+
+// testAdjacent2
+template<>
+template<>
+void object::test<1> ()
+{
+    checkLocation(
+        "GEOMETRYCOLLECTION (POLYGON ((1 9, 5 9, 5 1, 1 1, 1 9)), POLYGON ((9 9, 9 1, 5 1, 5 9, 9 9)))",
+        5, 5, Location::INTERIOR
+        );
+}
+
+// testNonAdjacent
+template<>
+template<>
+void object::test<2> ()
+{
+    checkLocation(
+        "GEOMETRYCOLLECTION (POLYGON ((1 9, 4 9, 5 1, 1 1, 1 9)), POLYGON ((9 9, 9 1, 5 1, 5 9, 9 9)))",
+        5, 5, Location::BOUNDARY
+        );
+}
+
+// testAdjacent6WithFilledHoles
+template<>
+template<>
+void object::test<3> ()
+{
+    checkLocation(
+        "GEOMETRYCOLLECTION (POLYGON ((1 9, 5 9, 6 6, 1 5, 1 9), (2 6, 4 8, 6 6, 2 6)), POLYGON ((2 6, 4 8, 6 6, 2 6)), POLYGON ((9 9, 9 5, 6 6, 5 9, 9 9)), POLYGON ((9 1, 5 1, 6 6, 9 5, 9 1), (7 2, 6 6, 8 3, 7 2)), POLYGON ((7 2, 6 6, 8 3, 7 2)), POLYGON ((1 1, 1 5, 6 6, 5 1, 1 1)))",
+        6, 6, Location::INTERIOR
+        );
+}
+
+// testAdjacent5WithEmptyHole
+template<>
+template<>
+void object::test<4> ()
+{
+    checkLocation(
+        "GEOMETRYCOLLECTION (POLYGON ((1 9, 5 9, 6 6, 1 5, 1 9), (2 6, 4 8, 6 6, 2 6)), POLYGON ((2 6, 4 8, 6 6, 2 6)), POLYGON ((9 9, 9 5, 6 6, 5 9, 9 9)), POLYGON ((9 1, 5 1, 6 6, 9 5, 9 1), (7 2, 6 6, 8 3, 7 2)), POLYGON ((1 1, 1 5, 6 6, 5 1, 1 1)))",
+        6, 6, Location::BOUNDARY
+        );
+}
+
+// testContainedAndAdjacent
+template<>
+template<>
+void object::test<5> ()
+{
+    std::string wkt("GEOMETRYCOLLECTION (POLYGON ((1 9, 9 9, 9 1, 1 1, 1 9)), POLYGON ((9 2, 2 2, 2 8, 9 8, 9 2)))");
+    checkLocation(wkt,
+        9, 5, Location::BOUNDARY
+        );
+    checkLocation(wkt,
+        9, 8, Location::BOUNDARY
+        );
+}
+
+/**
+* Tests a bug caused by incorrect point-on-segment logic.
+*/
+// testDisjointCollineartemplate<>
+template<>
+template<>
+void object::test<6> ()
+{
+    checkLocation(
+        "GEOMETRYCOLLECTION (MULTIPOLYGON (((1 4, 4 4, 4 1, 1 1, 1 4)), ((5 4, 8 4, 8 1, 5 1, 5 4))))",
+        2, 4, Location::BOUNDARY
+        );
+}
+
+
+
+
+
+} // namespace tut
diff --git a/tests/unit/operation/relateng/LinearBoundaryTest.cpp b/tests/unit/operation/relateng/LinearBoundaryTest.cpp
new file mode 100644
index 000000000..620c3bcb9
--- /dev/null
+++ b/tests/unit/operation/relateng/LinearBoundaryTest.cpp
@@ -0,0 +1,147 @@
+//
+// Test Suite for geos::operation::relateng::LinearBoundary class.
+
+#include <tut/tut.hpp>
+#include <utility.h>
+
+// geos
+#include <geos/algorithm/BoundaryNodeRule.h>
+#include <geos/geom/Coordinate.h>
+#include <geos/geom/CoordinateSequence.h>
+#include <geos/geom/Geometry.h>
+#include <geos/geom/Location.h>
+#include <geos/io/WKTReader.h>
+//#include <geos/io/WKTWriter.h>
+#include <geos/operation/relateng/LinearBoundary.h>
+#include <geos/operation/relateng/LineStringExtracter.h>
+
+// std
+#include <memory>
+
+using namespace geos::geom;
+using namespace geos::operation::relateng;
+using geos::algorithm::BoundaryNodeRule;
+using geos::io::WKTReader;
+// using geos::io::WKTWriter;
+
+namespace tut {
+//
+// Test Group
+//
+
+// Common data used by all tests
+struct test_linearboundary_data {
+
+    WKTReader r;
+    // WKTWriter w;
+
+    void
+    checkLinearBoundary(const std::string& wkt, const BoundaryNodeRule& bnr, const std::string& wktBdyExpected)
+    {
+        std::unique_ptr<Geometry> geom = r.read(wkt);
+        auto lines = extractLines(*geom);
+        LinearBoundary lb(lines, bnr);
+        bool hasBoundaryExpected = wktBdyExpected.length() == 0 ? false : true;
+        ensure_equals("hasBoundaryExpected == lb.hasBoundary", hasBoundaryExpected, lb.hasBoundary());
+
+        checkBoundaryPoints(lb, *geom, wktBdyExpected);
+    }
+
+    void
+    checkBoundaryPoints(LinearBoundary& lb, Geometry& geom, const std::string& wktBdyExpected)
+    {
+        std::set<CoordinateXY> bdySet;
+        extractPoints(wktBdyExpected, bdySet);
+
+        for (const CoordinateXY& p : bdySet) {
+            ensure("checkBoundaryPoints1", lb.isBoundary(&p));
+        }
+
+        auto allPts = geom.getCoordinates();
+        for (std::size_t i = 0; i < allPts->size(); i++) {
+            CoordinateXY p;
+            allPts->getAt(i, p);
+            if (bdySet.find(p) == bdySet.end()) { // p not in bdySet
+                ensure("checkBoundaryPoints2", !lb.isBoundary(&p));
+            }
+        }
+    }
+
+    void
+    extractPoints(const std::string& wkt, std::set<CoordinateXY>& ptSet)
+    {
+        if (wkt.length() == 0) return;
+        auto geom = r.read(wkt);
+        auto pts = geom->getCoordinates();
+        for (std::size_t i = 0; i < pts->size(); i++) {
+            CoordinateXY p;
+            pts->getAt(i, p);
+            ptSet.insert(p);
+        }
+        return;
+    }
+
+    std::vector<const LineString*>
+    extractLines(const Geometry& geom)
+    {
+        return LineStringExtracter::getLines(&geom);
+    }
+
+};
+
+typedef test_group<test_linearboundary_data> group;
+typedef group::object object;
+
+group test_linearboundary_group("geos::operation::relateng::LinearBoundary");
+
+//
+// Test Cases
+//
+
+// testLineMod2
+template<>
+template<>
+void object::test<1> ()
+{
+    checkLinearBoundary("LINESTRING (0 0, 9 9)",
+        BoundaryNodeRule::getBoundaryRuleMod2(),
+        "MULTIPOINT((0 0), (9 9))");
+}
+
+// testLines2Mod2
+template<>
+template<>
+void object::test<2> ()
+{
+    checkLinearBoundary("MULTILINESTRING ((0 0, 9 9), (9 9, 5 1))",
+        BoundaryNodeRule::getBoundaryRuleMod2(),
+        "MULTIPOINT((0 0), (5 1))");
+}
+
+// testLines3Mod2
+template<>
+template<>
+void object::test<3> ()
+{
+    checkLinearBoundary("MULTILINESTRING ((0 0, 9 9), (9 9, 5 1), (9 9, 1 5))",
+        BoundaryNodeRule::getBoundaryRuleMod2(),
+        "MULTIPOINT((0 0), (5 1), (1 5), (9 9))");
+}
+
+// testLines3Monvalent
+template<>
+template<>
+void object::test<4> ()
+{
+    checkLinearBoundary("MULTILINESTRING ((0 0, 9 9), (9 9, 5 1), (9 9, 1 5))",
+        BoundaryNodeRule::getBoundaryMonovalentEndPoint(),
+        "MULTIPOINT((0 0), (5 1), (1 5))");
+}
+
+
+
+
+
+
+
+} // namespace tut
diff --git a/tests/unit/operation/relateng/PolygonNodeConverterTest.cpp b/tests/unit/operation/relateng/PolygonNodeConverterTest.cpp
new file mode 100644
index 000000000..2d3ee22d7
--- /dev/null
+++ b/tests/unit/operation/relateng/PolygonNodeConverterTest.cpp
@@ -0,0 +1,212 @@
+//
+// Test Suite for geos::operation::relateng::PolygonNodeConverter class.
+
+#include <tut/tut.hpp>
+#include <utility.h>
+
+// geos
+#include <geos/io/WKTReader.h>
+#include <geos/io/WKTWriter.h>
+#include <geos/geom/Geometry.h>
+#include <geos/geom/Location.h>
+#include <geos/geom/Dimension.h>
+#include <geos/operation/relateng/PolygonNodeConverter.h>
+#include <geos/algorithm/PolygonNodeTopology.h>
+
+
+// std
+#include <memory>
+
+using namespace geos::geom;
+using namespace geos::operation::relateng;
+using geos::algorithm::PolygonNodeTopology;
+using geos::io::WKTReader;
+// using geos::io::WKTWriter;
+
+namespace tut {
+//
+// Test Group
+//
+
+// Common data used by all tests
+struct test_polygonnodeconverter_data {
+
+    NodeSection* sectionShell(double v0x, double v0y, double nx, double ny, double v1x, double v1y) {
+        return section(0, v0x, v0y, nx, ny, v1x, v1y);
+    }
+
+    NodeSection* sectionHole(double v0x, double v0y, double nx, double ny, double v1x, double v1y) {
+        return section(1, v0x, v0y, nx, ny, v1x, v1y);
+    }
+
+    NodeSection* section(int ringId, double v0x, double v0y, double nx, double ny, double v1x, double v1y) {
+        return new NodeSection(true, Dimension::A, 1, ringId, nullptr, false, 
+            new Coordinate(v0x, v0y), Coordinate(nx, ny), new Coordinate(v1x, v1y));
+    }
+
+    std::vector<const NodeSection*> 
+    toPtrVector(const std::vector<std::unique_ptr<NodeSection>>& input)
+    {
+        std::vector<const NodeSection*> vec;
+        for(std::size_t i = 0, n = input.size(); i < n; ++i) {
+            vec.emplace_back(input[i].get());
+        }
+        return vec;
+    }
+
+    bool checkSectionsEqual(std::vector<const NodeSection*>& ns1, 
+                            std::vector<const NodeSection*>& ns2) {
+        if (ns1.size() != ns2.size())
+            return false;
+        sort(ns1);
+        sort(ns2);
+        for (std::size_t i = 0; i < ns1.size(); i++) {
+            int comp = ns1[i]->compareTo(ns2[i]);
+            if (comp != 0)
+                return false;
+        }
+        return true;
+    }
+    void sort(std::vector<const NodeSection*>& ns) {
+
+        // Comparator lambda for sort support
+        auto comparator = [](
+            const NodeSection* a,
+            const NodeSection* b)
+        {
+            return a->compareTo(b) < 0;
+        };
+            
+        std::sort(ns.begin(), ns.end(), comparator);
+    }
+
+    void 
+    freeNodeSections(std::vector<const NodeSection*>& ns) {
+        for (std::size_t i = 0; i < ns.size(); i++) {
+            delete ns[i]->getVertex(0);
+            delete ns[i]->getVertex(1);
+            delete ns[i];
+        }
+    }
+
+    void
+    checkConversion(std::vector<const NodeSection *>& input, 
+                    std::vector<const NodeSection *>& expected)
+    {
+        auto actual = PolygonNodeConverter::convert( input );
+        auto actualPtr = toPtrVector(actual);
+        bool isEqual = checkSectionsEqual(actualPtr, expected);
+        freeNodeSections(input);
+        freeNodeSections(expected);
+        /*
+        if (! isEqual) {
+            System.out.println("Expected:" + formatSections(expected));
+            System.out.println("Actual:" + formatSections(actual));      
+        }
+        */
+        ensure("checkConversion", isEqual);
+    }
+
+};
+
+typedef test_group<test_polygonnodeconverter_data> group;
+typedef group::object object;
+
+group test_polygonnodeconverter_group("geos::operation::relateng::PolygonNodeConverter");
+
+//
+// Test Cases
+//
+
+
+// testShells
+template<>
+template<>
+void object::test<1> ()
+{
+    std::vector<const NodeSection *> input{
+        sectionShell( 1,1, 5,5, 9,9 ),
+        sectionShell( 8,9, 5,5, 6,9 ),
+        sectionShell( 4,9, 5,5, 2,9 )
+    };
+    std::vector<const NodeSection *> expected{
+        sectionShell( 1,1, 5,5, 9,9 ),
+        sectionShell( 8,9, 5,5, 6,9 ),
+        sectionShell( 4,9, 5,5, 2,9 )
+    };
+    checkConversion(input, expected);
+}
+
+// testShellAndHole
+template<>
+template<>
+void object::test<2> ()
+{
+    std::vector<const NodeSection *> input{
+            sectionShell( 1,1, 5,5, 9,9 ),
+            sectionHole(  6,0, 5,5, 4,0 )
+    };
+    std::vector<const NodeSection *> expected{
+            sectionShell( 1,1, 5,5, 4,0 ),
+            sectionShell( 6,0, 5,5, 9,9 )
+   };
+    checkConversion(input, expected);
+}
+
+// testShellsAndHoles
+template<>
+template<>
+void object::test<3> ()
+{
+    std::vector<const NodeSection *> input{
+        sectionShell( 1,1, 5,5, 9,9 ),
+        sectionHole(  6,0, 5,5, 4,0 ),
+        
+        sectionShell( 8,8, 5,5, 1,8 ),
+        sectionHole(  4,8, 5,5, 6,8 )
+    };
+    std::vector<const NodeSection *> expected{
+        sectionShell( 1,1, 5,5, 4,0 ),
+        sectionShell( 6,0, 5,5, 9,9 ),
+        
+        sectionShell( 4,8, 5,5, 1,8 ),
+        sectionShell( 8,8, 5,5, 6,8 )
+   };
+    checkConversion(input, expected);
+}
+
+// testShellAnd2Holes
+template<>
+template<>
+void object::test<5> ()
+{
+    std::vector<const NodeSection *> input{
+        sectionShell( 1,1, 5,5, 9,9 ),
+        sectionHole(  7,0, 5,5, 6,0 ),
+        sectionHole(  4,0, 5,5, 3,0 )
+    };
+    std::vector<const NodeSection *> expected{
+        sectionShell( 1,1, 5,5, 3,0 ),
+        sectionShell( 4,0, 5,5, 6,0 ),
+        sectionShell( 7,0, 5,5, 9,9 )
+    };
+    checkConversion(input, expected);
+}
+
+// testHoles
+template<>
+template<>
+void object::test<6> ()
+{
+    std::vector<const NodeSection *> input{
+        sectionHole(  7,0, 5,5, 6,0 ),
+        sectionHole(  4,0, 5,5, 3,0 )
+    };
+    std::vector<const NodeSection *> expected{
+        sectionShell( 4,0, 5,5, 6,0 ),
+        sectionShell( 7,0, 5,5, 3,0 )
+    };
+    checkConversion(input, expected);
+}
+
+} // namespace tut
diff --git a/tests/unit/operation/relateng/RelateGeometryTest.cpp b/tests/unit/operation/relateng/RelateGeometryTest.cpp
new file mode 100644
index 000000000..8ecd91e1f
--- /dev/null
+++ b/tests/unit/operation/relateng/RelateGeometryTest.cpp
@@ -0,0 +1,138 @@
+//
+// Test Suite for geos::operation::relateng::RelateGeometry class.
+
+#include <tut/tut.hpp>
+#include <utility.h>
+
+// geos
+#include <geos/io/WKTReader.h>
+#include <geos/io/WKTWriter.h>
+#include <geos/geom/Geometry.h>
+#include <geos/geom/Location.h>
+#include <geos/operation/relateng/RelateGeometry.h>
+#include <geos/operation/relateng/DimensionLocation.h>
+
+// std
+#include <memory>
+
+using namespace geos::geom;
+using namespace geos::operation::relateng;
+using geos::io::WKTReader;
+// using geos::io::WKTWriter;
+
+namespace tut {
+//
+// Test Group
+//
+
+// Common data used by all tests
+struct test_relategeometry_data {
+
+    WKTReader r;
+    // WKTWriter w;
+
+    void checkDimension(const std::string& wkt, int expectedDim, int expectedDimReal)
+    {
+        std::unique_ptr<Geometry> geom = r.read(wkt);
+        RelateGeometry rgeom(geom.get());
+        ensure_equals("checkNodeLocation 1", rgeom.getDimension(), expectedDim);
+        ensure_equals("checkNodeLocation 2", rgeom.getDimensionReal(), expectedDimReal);
+    }
+
+};
+
+typedef test_group<test_relategeometry_data> group;
+typedef group::object object;
+
+group test_relategeometry_group("geos::operation::relateng::RelateGeometry");
+
+
+// testUniquePoints
+template<>
+template<>
+void object::test<1> ()
+{
+    std::unique_ptr<Geometry> geom = r.read("MULTIPOINT ((0 0), (5 5), (5 0), (0 0))");
+    RelateGeometry rgeom(geom.get());
+    auto pts = rgeom.getUniquePoints();
+    ensure_equals("Unique pts size", pts.size(), 3u);
+}
+
+// testBoundary
+template<>
+template<>
+void object::test<2> ()
+{
+    std::unique_ptr<Geometry> geom = r.read("MULTILINESTRING ((0 0, 9 9), (9 9, 5 1))");
+    RelateGeometry rgeom(geom.get());
+    ensure("hasBoundary", rgeom.hasBoundary());
+}
+
+// testHasDimension
+template<>
+template<>
+void object::test<3> ()
+{
+    std::unique_ptr<Geometry> geom = r.read("GEOMETRYCOLLECTION (POLYGON ((1 9, 5 9, 5 5, 1 5, 1 9)), LINESTRING (1 1, 5 4), POINT (6 5))");
+    RelateGeometry rgeom(geom.get());
+    ensure("hasDimension 0", rgeom.hasDimension(0));
+    ensure("hasDimension 1", rgeom.hasDimension(1));
+    ensure("hasDimension 2", rgeom.hasDimension(2));
+}
+
+// testDimension
+template<>
+template<>
+void object::test<4> ()
+{
+    checkDimension("POINT (0 0)", 0, 0);
+}
+
+template<>
+template<>
+void object::test<5> ()
+{
+    checkDimension("LINESTRING (0 0, 0 0)", 1, 0);
+}
+
+template<>
+template<>
+void object::test<6> ()
+{
+    checkDimension("LINESTRING (0 0, 9 9)", 1, 1);
+}
+
+template<>
+template<>
+void object::test<7> ()
+{
+    checkDimension("POLYGON ((1 9, 5 9, 5 5, 1 5, 1 9))", 2, 2);
+}
+
+template<>
+template<>
+void object::test<8> ()
+{
+    checkDimension("GEOMETRYCOLLECTION (POLYGON ((1 9, 5 9, 5 5, 1 5, 1 9)), LINESTRING (1 1, 5 4), POINT (6 5))", 2, 2);
+}
+
+template<>
+template<>
+void object::test<9> ()
+{
+    checkDimension("GEOMETRYCOLLECTION (POLYGON EMPTY, LINESTRING (1 1, 5 4), POINT (6 5))", 2, 1);
+}
+
+template<>
+template<>
+void object::test<10> ()
+{
+    checkDimension("LINESTRING (0 0, 0 0, 9 9)", 1, 1);
+}
+
+
+
+
+
+
+} // namespace tut
diff --git a/tests/unit/operation/relateng/RelateNGGCTest.cpp b/tests/unit/operation/relateng/RelateNGGCTest.cpp
new file mode 100644
index 000000000..5ca0bf8ff
--- /dev/null
+++ b/tests/unit/operation/relateng/RelateNGGCTest.cpp
@@ -0,0 +1,315 @@
+//
+// Test Suite for geos::operation::relateng::RelateNG class.
+
+#include <tut/tut.hpp>
+#include <utility.h>
+
+// geos
+#include "RelateNGTest.h"
+
+using namespace geos::geom;
+using namespace geos::operation::relateng;
+using geos::io::WKTReader;
+using geos::io::WKTWriter;
+
+namespace tut {
+//
+// Test Group
+//
+
+// Common data used by all tests
+struct test_relatenggc_data : test_relateng_support {
+
+    const char* wktAdjacentPolys = "GEOMETRYCOLLECTION (POLYGON ((5 5, 2 9, 9 9, 9 5, 5 5)), POLYGON ((3 1, 5 5, 9 5, 9 1, 3 1)), POLYGON ((1 9, 2 9, 5 5, 3 1, 1 1, 1 9)))";
+
+};
+
+
+typedef test_group<test_relatenggc_data> group;
+typedef group::object object;
+
+group test_relatenggc_group("geos::operation::relateng::RelateNGGC");
+
+
+// testDimensionWithEmpty
+template<>
+template<>
+void object::test<1> ()
+{
+    std::string a = "LINESTRING(0 0, 1 1)";
+    std::string b = "GEOMETRYCOLLECTION(POLYGON EMPTY,LINESTRING(0 0, 1 1))";
+    checkCoversCoveredBy(a, b, true);
+    checkEquals(a, b, true);
+}
+  
+// see https://github.com/libgeos/geos/issues/1027
+// testMP_GLP_GEOS1027
+template<>
+template<>
+void object::test<2> ()
+{
+    std::string a = "MULTIPOLYGON (((0 0, 3 0, 3 3, 0 3, 0 0)))";
+    std::string b = "GEOMETRYCOLLECTION ( LINESTRING (1 2, 1 1), POINT (0 0))";
+    checkRelate(a, b, "1020F1FF2");
+    checkIntersectsDisjoint(a, b, true);
+    checkContainsWithin(a, b, true);
+    checkCrosses(a, b, false);
+    checkEquals(a, b, false);
+}
+  
+// see https://github.com/libgeos/geos/issues/1022
+// testGPL_A
+template<>
+template<>
+void object::test<3> ()
+{
+    std::string a = "GEOMETRYCOLLECTION (POINT (7 1), LINESTRING (6 5, 6 4))";
+    std::string b = "POLYGON ((7 1, 1 3, 3 9, 7 1))";
+    checkRelate(a, b, "F01FF0212");
+    checkIntersectsDisjoint(a, b, true);
+    checkContainsWithin(a, b, false);
+    checkCrosses(a, b, false);
+    checkTouches(a, b, true);
+    checkEquals(a, b, false);
+}
+  
+// see https://github.com/libgeos/geos/issues/982
+// testP_GPL
+template<>
+template<>
+void object::test<4> ()
+{
+    std::string a = "POINT(0 0)";
+    std::string b = "GEOMETRYCOLLECTION(POINT(0 0), LINESTRING(0 0, 1 0))";
+    checkRelate(a, b, "F0FFFF102");
+    checkIntersectsDisjoint(a, b, true);
+    checkContainsWithin(a, b, false);
+    checkCrosses(a, b, false);
+    checkTouches(a, b, true);
+    checkEquals(a, b, false);
+}
+  
+// testLineInOverlappingPolygonsTouchingInteriorEdge
+template<>
+template<>
+void object::test<5> ()
+{
+    std::string a = "LINESTRING (3 7, 7 3)";
+    std::string b = "GEOMETRYCOLLECTION (POLYGON ((1 9, 7 9, 7 3, 1 3, 1 9)), POLYGON ((9 1, 3 1, 3 7, 9 7, 9 1)))";
+    checkRelate(a, b, "1FF0FF212");
+    checkContainsWithin(b, a, true);
+}
+  
+// testLineInOverlappingPolygonsCrossingInteriorEdgeAtVertex
+template<>
+template<>
+void object::test<6> ()
+{
+    std::string a = "LINESTRING (2 2, 8 8)";
+    std::string b = "GEOMETRYCOLLECTION (POLYGON ((1 1, 1 7, 7 7, 7 1, 1 1)), POLYGON ((9 9, 9 3, 3 3, 3 9, 9 9)))";
+    checkRelate(a, b, "1FF0FF212");
+    checkContainsWithin(b, a, true);
+}
+  
+// testLineInOverlappingPolygonsCrossingInteriorEdgeProper
+template<>
+template<>
+void object::test<7> ()
+{
+    std::string a = "LINESTRING (2 4, 6 8)";
+    std::string b = "GEOMETRYCOLLECTION (POLYGON ((1 1, 1 7, 7 7, 7 1, 1 1)), POLYGON ((9 9, 9 3, 3 3, 3 9, 9 9)))";
+    checkRelate(a, b, "1FF0FF212");
+    checkContainsWithin(b, a, true);
+}
+  
+// testPolygonInOverlappingPolygonsTouchingBoundaries
+template<>
+template<>
+void object::test<8> ()
+{
+    std::string a = "GEOMETRYCOLLECTION (POLYGON ((1 9, 6 9, 6 4, 1 4, 1 9)), POLYGON ((9 1, 4 1, 4 6, 9 6, 9 1)) )";
+    std::string b = "POLYGON ((2 6, 6 2, 8 4, 4 8, 2 6))";
+    checkRelate(a, b, "212F01FF2");
+    checkContainsWithin(a, b, true);
+}
+  
+// testLineInOverlappingPolygonsBoundaries
+template<>
+template<>
+void object::test<9> ()
+{
+    std::string a = "LINESTRING (1 6, 9 6, 9 1, 1 1, 1 6)";
+    std::string b = "GEOMETRYCOLLECTION (POLYGON ((1 1, 1 6, 6 6, 6 1, 1 1)), POLYGON ((9 1, 4 1, 4 6, 9 6, 9 1)))";
+    checkRelate(a, b, "F1FFFF2F2");
+    checkContainsWithin(a, b, false);
+    checkCoversCoveredBy(a, b, false);
+    checkCoversCoveredBy(b, a, true);
+}
+  
+// testLineCoversOverlappingPolygonsBoundaries
+template<>
+template<>
+void object::test<10> ()
+{
+    std::string a = "LINESTRING (1 6, 9 6, 9 1, 1 1, 1 6)";
+    std::string b = "GEOMETRYCOLLECTION (POLYGON ((1 1, 1 6, 6 6, 6 1, 1 1)), POLYGON ((9 1, 4 1, 4 6, 9 6, 9 1)))";
+    checkRelate(a, b, "F1FFFF2F2");
+    checkContainsWithin(b, a, false);
+    checkCoversCoveredBy(b, a, true);
+}
+  
+// testAdjacentPolygonsContainedInAdjacentPolygons
+template<>
+template<>
+void object::test<11> ()
+{
+    std::string a = "GEOMETRYCOLLECTION (POLYGON ((2 2, 2 5, 4 5, 4 2, 2 2)), POLYGON ((8 2, 4 3, 4 4, 8 5, 8 2)))";
+    std::string b = "GEOMETRYCOLLECTION (POLYGON ((1 1, 1 6, 4 6, 4 1, 1 1)), POLYGON ((9 1, 4 1, 4 6, 9 6, 9 1)))";
+    checkRelate(a, b, "2FF1FF212");
+    checkContainsWithin(b, a, true);
+    checkCoversCoveredBy(b, a, true);
+}
+  
+// testGCMultiPolygonIntersectsPolygon
+template<>
+template<>
+void object::test<12> ()
+{
+    std::string a = "POLYGON ((2 5, 3 5, 3 3, 2 3, 2 5))";
+    std::string b = "GEOMETRYCOLLECTION (MULTIPOLYGON (((1 4, 4 4, 4 1, 1 1, 1 4)), ((5 4, 8 4, 8 1, 5 1, 5 4))))";
+    checkRelate(a, b, "212101212");
+    checkIntersectsDisjoint(a, b, true);
+    checkCoversCoveredBy(b, a, false);
+}
+  
+
+// testPolygonContainsGCMultiPolygonElement
+template<>
+template<>
+void object::test<13> ()
+{
+    std::string a = "POLYGON ((0 5, 4 5, 4 1, 0 1, 0 5))";
+    std::string b = "GEOMETRYCOLLECTION (MULTIPOLYGON (((1 4, 3 4, 3 2, 1 2, 1 4)), ((6 4, 8 4, 8 2, 6 2, 6 4))))";
+    checkRelate(a, b, "212FF1212");
+    checkIntersectsDisjoint(a, b, true);
+    checkCoversCoveredBy(b, a, false);
+}
+
+  /**
+   * Demonstrates the need for assigning computed nodes to their rings,
+   * so that subsequent PIP testing can report node as being on ring boundary.
+   */
+// testPolygonOverlappingGCPolygon
+template<>
+template<>
+void object::test<14> ()
+{
+    std::string a = "GEOMETRYCOLLECTION (POLYGON ((18.6 40.8, 16.8825 39.618567, 16.9319 39.5461, 17.10985 39.485133, 16.6143 38.4302, 16.43145 38.313267, 16.2 37.5, 14.8 37.8, 14.96475 40.474933, 18.6 40.8)))";
+    std::string b = "POLYGON ((16.3649953125 38.37219358064516, 16.3649953125 39.545924774193544, 17.949465625000002 39.545924774193544, 17.949465625000002 38.37219358064516, 16.3649953125 38.37219358064516))";
+    checkRelate(b, a, "212101212");
+    checkRelate(a, b, "212101212");
+    checkIntersectsDisjoint(a, b, true);
+    checkCoversCoveredBy(a, b, false);
+}
+  
+
+// testAdjPolygonsCoverPolygonWithEndpointInside
+template<>
+template<>
+void object::test<15> ()
+{
+    std::string a = wktAdjacentPolys;
+    std::string b = "POLYGON ((3 7, 7 7, 7 3, 3 3, 3 7))";
+    checkRelate(b, a, "2FF1FF212");
+    checkRelate(a, b, "212FF1FF2");
+    checkIntersectsDisjoint(a, b, true);
+    checkCoversCoveredBy(a, b, true);
+}
+  
+// testAdjPolygonsCoverPointAtNode
+template<>
+template<>
+void object::test<16> ()
+{
+    std::string a = wktAdjacentPolys;
+    std::string b = "POINT (5 5)";
+    checkRelate(b, a, "0FFFFF212");
+    checkRelate(a, b, "0F2FF1FF2");
+    checkIntersectsDisjoint(a, b, true);
+    checkCoversCoveredBy(a, b, true);
+}
+  
+// testAdjPolygonsCoverPointOnEdge
+template<>
+template<>
+void object::test<17> ()
+{
+    std::string a = wktAdjacentPolys;
+    std::string b = "POINT (7 5)";
+    checkRelate(b, a, "0FFFFF212");
+    checkRelate(a, b, "0F2FF1FF2");
+    checkIntersectsDisjoint(a, b, true);
+    checkCoversCoveredBy(a, b, true);
+}
+  
+// testAdjPolygonsContainingPolygonTouchingInteriorEndpoint
+template<>
+template<>
+void object::test<18> ()
+{
+    std::string a = wktAdjacentPolys;
+    std::string b = "POLYGON ((5 5, 7 5, 7 3, 5 3, 5 5))";
+    checkRelate(a, b, "212FF1FF2");
+    checkIntersectsDisjoint(a, b, true);
+    checkCoversCoveredBy(a, b, true);
+}
+  
+// testAdjPolygonsOverlappedByPolygonWithHole
+template<>
+template<>
+void object::test<19> ()
+{
+    std::string a = wktAdjacentPolys;
+    std::string b = "POLYGON ((0 10, 10 10, 10 0, 0 0, 0 10), (2 8, 8 8, 8 2, 2 2, 2 8))";
+    checkRelate(a, b, "2121FF212");
+    checkIntersectsDisjoint(a, b, true);
+    checkCoversCoveredBy(a, b, false);
+}
+  
+// testAdjPolygonsContainingLine
+template<>
+template<>
+void object::test<20> ()
+{
+    std::string a = wktAdjacentPolys;
+    std::string b = "LINESTRING (5 5, 7 7)";
+    checkRelate(a, b, "102FF1FF2");
+    checkIntersectsDisjoint(a, b, true);
+    checkCoversCoveredBy(a, b, true);
+}
+  
+// testAdjPolygonsContainingLineAndPoint
+template<>
+template<>
+void object::test<21> ()
+{
+    std::string a = wktAdjacentPolys;
+    std::string b = "GEOMETRYCOLLECTION (POINT (5 5), LINESTRING (5 7, 7 7))";
+    checkRelate(a, b, "102FF1FF2");
+    checkIntersectsDisjoint(a, b, true);
+    checkCoversCoveredBy(a, b, true);
+}
+  
+// https://trac.osgeo.org/geos/ticket/1110
+template<>
+template<>
+void object::test<22> ()
+{
+    std::string a = "POLYGON ((3 7, 7 7, 7 3, 3 3, 3 7))";
+    std::string b = "GEOMETRYCOLLECTION (MULTIPOINT (EMPTY, (5 5)), LINESTRING (1 9, 4 9))";
+    checkIntersectsDisjoint(a, b, true);
+}
+
+
+
+} // namespace tut
diff --git a/tests/unit/operation/relateng/RelateNGRobustnessTest.cpp b/tests/unit/operation/relateng/RelateNGRobustnessTest.cpp
new file mode 100644
index 000000000..6761655f4
--- /dev/null
+++ b/tests/unit/operation/relateng/RelateNGRobustnessTest.cpp
@@ -0,0 +1,282 @@
+//
+// Test Suite for geos::operation::relateng::RelateNG class.
+
+#include <tut/tut.hpp>
+#include <utility.h>
+
+// geos
+#include "RelateNGTest.h"
+
+using namespace geos::geom;
+using namespace geos::operation::relateng;
+using geos::io::WKTReader;
+using geos::io::WKTWriter;
+
+namespace tut {
+//
+// Test Group
+//
+
+// Common data used by all tests
+struct test_relatengrobuestness_data : test_relateng_support {};
+
+
+typedef test_group<test_relatengrobuestness_data> group;
+typedef group::object object;
+
+group test_relatengrobuestness_group("geos::operation::relateng::RelateNGRobustness");
+
+
+//--------------------------------------------------------
+//  GeometryCollection semantics
+//--------------------------------------------------------
+
+// see https://github.com/libgeos/geos/issues/1033
+// testGEOS_1033
+template<>
+template<>
+void object::test<1> ()
+{
+    checkContainsWithin("POLYGON((1 0,0 4,2 2,1 0))",
+        "GEOMETRYCOLLECTION(POINT(2 2),POINT(1 0),LINESTRING(1 2,1 1))",
+        true);
+}
+
+// https://github.com/libgeos/geos/issues/1027
+// testGEOS_1027
+template<>
+template<>
+void object::test<2> ()
+{
+    checkCoversCoveredBy("MULTIPOLYGON (((0 0, 3 0, 3 3, 0 3, 0 0)))",
+        "GEOMETRYCOLLECTION ( LINESTRING (1 2, 1 1), POINT (0 0))",
+        true);
+}
+
+// https://github.com/libgeos/geos/issues/1022
+// testGEOS_1022
+template<>
+template<>
+void object::test<3> ()
+{
+    checkCrosses("GEOMETRYCOLLECTION (POINT (7 1), LINESTRING (6 5, 6 4))",
+        "POLYGON ((7 1, 1 3, 3 9, 7 1))",
+        false);
+}
+
+// https://github.com/libgeos/geos/issues/1011
+// testGEOS_1011
+template<>
+template<>
+void object::test<4> ()
+{
+    std::string a = "LINESTRING(75 15,55 43)";
+    std::string b = "GEOMETRYCOLLECTION(POLYGON EMPTY,LINESTRING(75 15,55 43))";
+    checkCoversCoveredBy(a, b, true);
+    checkEquals(a, b, true);
+}
+
+// https://github.com/libgeos/geos/issues/983
+// testGEOS_983
+template<>
+template<>
+void object::test<5> ()
+{
+    std::string a = "POINT(0 0)";
+    std::string b = "GEOMETRYCOLLECTION(POINT (1 1), LINESTRING (1 1, 2 2))";
+    checkIntersectsDisjoint(a, b, false);
+}
+
+// https://github.com/libgeos/geos/issues/982
+// testGEOS_982
+template<>
+template<>
+void object::test<6> ()
+{
+    std::string a = "POINT(0 0)";
+    std::string b1 = "GEOMETRYCOLLECTION(POINT(0 0), LINESTRING(0 0, 1 0))";
+    checkContainsWithin(b1, a, false);
+    checkCoversCoveredBy(b1, a, true);
+
+    std::string b2 = "GEOMETRYCOLLECTION(LINESTRING(0 0, 1 0), POINT(0 0))";
+    checkContainsWithin(b2, a, false);
+    checkCoversCoveredBy(b2, a, true);
+}
+
+// https://github.com/libgeos/geos/issues/981
+// testGEOS_981
+template<>
+template<>
+void object::test<7> ()
+{
+    std::string a = "POINT(0 0)";
+    std::string b = "GEOMETRYCOLLECTION(LINESTRING(0 1, 0 0), POINT(0 0))";
+    checkRelateMatches(b, a, IntersectionMatrixPattern::CONTAINS_PROPERLY, false);
+}
+
+
+//--------------------------------------------------------
+//  Noding robustness problems
+//--------------------------------------------------------
+
+// https://github.com/libgeos/geos/issues/1053
+// testGEOS_1053
+template<>
+template<>
+void object::test<8> ()
+{
+    std::string a = "MULTILINESTRING((2 4, 10 10),(15 10,10 5,5 10))";
+    std::string b = "MULTILINESTRING((2 4, 10 10))";
+    checkRelate(a, b, "1F1F00FF2");
+}
+
+// https://github.com/libgeos/geos/issues/968
+// testGEOS_968
+template<>
+template<>
+void object::test<9> ()
+{
+    std::string a2 = "LINESTRING(10 0, 0 20)";
+    std::string b2 = "POINT (9 2)";
+    checkCoversCoveredBy(a2, b2, true);
+}
+
+// xtestGEOS_968_2
+// commented out, related to deeper DD vs FP differences
+
+// template<>
+// template<>
+// void object::test<10> ()
+// {
+//     std::string a = "LINESTRING(1 0, 0 2)";
+//     std::string b = "POINT (0.9 0.2)";
+//     //-- this case doesn't work due to numeric rounding for Orientation test
+//     checkCoversCoveredBy(a, b,true);
+// }
+
+// https://github.com/libgeos/geos/issues/933
+// testGEOS_933
+template<>
+template<>
+void object::test<11> ()
+{
+    std::string a = "LINESTRING (0 0, 1 1)";
+    std::string b = "LINESTRING (0.2 0.2, 0.5 0.5)";
+    checkCoversCoveredBy(a, b, true);
+}
+
+// https://github.com/libgeos/geos/issues/740
+// testGEOS_740
+template<>
+template<>
+void object::test<12> ()
+{
+    std::string a = "POLYGON ((1454700 -331500, 1455100 -330700, 1455466.6191038645 -331281.94727476506, 1455467.8182005754 -331293.26796732045, 1454700 -331500))";
+    std::string b = "LINESTRING (1455389.376551584 -331255.3803222172, 1455467.2422460222 -331287.83037053316)";
+    checkContainsWithin(a, b, false);
+}
+
+//--------------------------------------------------------
+//  Robustness failures (TopologyException in old code)
+//--------------------------------------------------------
+
+// https://github.com/libgeos/geos/issues/766
+// testGEOS_766
+template<>
+template<>
+void object::test<13> ()
+{
+    std::string a = "POLYGON ((26639.240191093646 6039.3615818717535, 26639.240191093646 5889.361620883223, 28000.000095100608 5889.362081553552, 28000.000095100608 6039.361620882992, 28700.00019021402 6039.361620882992, 28700.00019021402 5889.361822800367, 29899.538842431968 5889.362160452064, 32465.59665091549 5889.362882757903, 32969.2837182586 -1313.697771558439, 31715.832811969216 -1489.87008918589, 31681.039836323587 -1242.3030298361555, 32279.3890331618 -1158.210534269224, 32237.63710287376 -861.1301136466199, 32682.89764107368 -802.0828534499739, 32247.445200905553 5439.292852892075, 31797.06861513178 5439.292852892075, 31797.06861513178 5639.36178850523, 29899.538849750803 5639.361268079038, 26167.69458275995 5639.3602445643955, 26379.03654594742 2617.0293071870683, 26778.062167926924 2644.9318977193907, 26792.01346261031 2445.419086759444, 26193.472956813417 2403.5650586598513, 25939.238114175267 6039.361685403233, 26639.240191093646 6039.3615818717535), (32682.89764107368
  -802.0828534499738, 32682.89764107378 -802.0828534499669, 32247.445200905655 5439.292852892082, 32247.445200905553 5439.292852892075, 32682.89764107368 -802.0828534499738))";
+    std::string b = "POLYGON ((32450.100392347143 5889.362314133216, 32050.104955691 5891.272957209961, 32100.021071878822 16341.272221116333, 32500.016508656867 16339.361578039587, 32450.100392347143 5889.362314133216))";
+    checkIntersectsDisjoint(a, b, true);
+}
+
+// https://github.com/libgeos/geos/issues/1026
+// testGEOS_1026
+template<>
+template<>
+void object::test<14> ()
+{
+    std::string a = "POLYGON((335645.7810000004 5677846.65,335648.6579999998 5677845.801999999,335650.8630842535 5677845.143617179,335650.77673334075 5677844.7250704905,335642.90299999993 5677847.498,335645.7810000004 5677846.65))";
+    std::string b = "POLYGON((335642.903 5677847.498,335642.894 5677847.459,335645.92 5677846.69,335647.378 5677852.523,335644.403 5677853.285,335644.374 5677853.293,335642.903 5677847.498))";
+    checkTouches(a, b, false);
+}
+
+// https://github.com/libgeos/geos/issues/1069 =- too large to reproduce here
+// https://trac.osgeo.org/postgis/ticket/5583 =- too large to reproduce here
+// https://github.com/locationtech/jts/issues/1051
+// testJTS_1051
+template<>
+template<>
+void object::test<15> ()
+{
+    std::string a = "POLYGON ((414188.5999999999 6422867.1, 414193.7 6422866.5, 414205.1 6422859.4, 414223.7 6422846.8, 414229.6 6422843.2, 414235.2 6422835.4, 414224.7 6422837.9, 414219.4 6422842.1, 414210.9 6422849, 414199.2 6422857.6, 414191.1 6422863.4, 414188.5999999999 6422867.1))";
+    std::string b = "LINESTRING (414187.2 6422831.6, 414179 6422836.1, 414182.2 6422841.8, 414176.7 6422844, 414184.5 6422859.5, 414188.6 6422867.1)";
+    checkIntersectsDisjoint(a, b, true);
+}
+
+// https://trac.osgeo.org/postgis/ticket/5362
+// testPostGIS_5362
+template<>
+template<>
+void object::test<16> ()
+{
+    std::string a = "POLYGON ((-707259.66 -1121493.36, -707205.9 -1121605.808, -707310.5388 -1121540.5446, -707318.8200000001 -1121533.21, -707259.66 -1121493.36))";
+    std::string b = "POLYGON ((-707356.18 -1121550.69, -707332.82 -1121536.63, -707318.82 -1121533.21, -707321.72 -1121535.08, -707327.4 -1121539.21, -707356.18 -1121550.69))";
+    checkRelate(a, b, "2F2101212");
+    checkIntersectsDisjoint(a, b, true);
+}
+
+//--------------------------------------------------------
+//  Topological Inconsistency
+//--------------------------------------------------------
+
+// https://github.com/libgeos/geos/issues/1064
+// testGEOS_1064
+template<>
+template<>
+void object::test<17> ()
+{
+    std::string a = "LINESTRING (16.330791631988802 68.75635661578073, 16.332533372319826 68.75496886016562)";
+    std::string b = "LINESTRING (16.30641253121884 68.75189557630306, 16.33167771310482 68.75565061843871)";
+    checkRelate(a, b, "F01FF0102");
+}
+
+// https://github.com/locationtech/jts/issues/396
+// testJTS_396
+template<>
+template<>
+void object::test<18> ()
+{
+    std::string a = "LINESTRING (1 0, 0 2, 0 0, 2 2)";
+    std::string b = "LINESTRING (0 0, 2 2)";
+    checkRelate(a, b, "101F00FF2");
+    checkCoversCoveredBy(a, b, true);
+}
+
+//https://github.com/locationtech/jts/issues/270
+// testJTS_270
+template<>
+template<>
+void object::test<19> ()
+{
+    std::string a = "LINESTRING(0.0 0.0, -10.0 1.2246467991473533E-15)";
+    std::string b = "LINESTRING(-9.999143275740073 -0.13089595571333978, -10.0 1.0535676356486768E-13)";
+    checkRelate(a, b, "FF10F0102");
+    checkIntersectsDisjoint(a, b, true);
+}
+
+//https://gis.stackexchange.com/questions/484691/topologyexception-side-location-conflict-while-intersects-on-valid-polygons
+//testGISSE_484691
+template<>
+template<>
+void object::test<20> ()
+{
+    std::string a = "POLYGON ((1.839012980156925 43.169860517728324, 1.838983490127865 43.169860200336274, 1.838898525601717 43.169868281549725, 1.838918565176068 43.1699719478626, 1.838920733577112 43.16998636433192, 1.838978629555589 43.16997979090823, 1.838982586839382 43.169966339940714, 1.838974943184281 43.169918580432174, 1.839020497362873 43.169914572864634, 1.839012980156925 43.169860517728324))";
+    std::string b = "POLYGON ((1.8391355300979277 43.16987802887805, 1.83913336164737 43.16986361241434, 1.8390129801569248 43.169860517728324, 1.8390790978572837 43.16987292371998, 1.8390909520103162 43.16995581178317, 1.8391377530291442 43.16995091801345, 1.8391293863398452 43.16987796276235, 1.8391355300979277 43.16987802887805))";
+    checkRelate(a, b, "2F2101212");
+    checkIntersectsDisjoint(a, b, true);
+}
+  
+
+} // namespace tut
diff --git a/tests/unit/operation/relateng/RelateNGTest.cpp b/tests/unit/operation/relateng/RelateNGTest.cpp
new file mode 100644
index 000000000..7e54c71c9
--- /dev/null
+++ b/tests/unit/operation/relateng/RelateNGTest.cpp
@@ -0,0 +1,879 @@
+//
+// Test Suite for geos::operation::relateng::RelateNG class.
+
+#include <tut/tut.hpp>
+#include <utility.h>
+
+// geos
+#include "RelateNGTest.h"
+
+using namespace geos::geom;
+using namespace geos::operation::relateng;
+using geos::io::WKTReader;
+using geos::io::WKTWriter;
+
+namespace tut {
+//
+// Test Group
+//
+
+// Common data used by all tests
+struct test_relateng_data : test_relateng_support {};
+
+
+typedef test_group<test_relateng_data> group;
+typedef group::object object;
+
+group test_relateng_group("geos::operation::relateng::RelateNG");
+
+
+// testPointsDisjoint
+template<>
+template<>
+void object::test<1> ()
+{
+    std::string a = "POINT (0 0)";
+    std::string b = "POINT (1 1)";
+    checkIntersectsDisjoint(a, b, false);
+    checkContainsWithin(a, b, false);
+    checkEquals(a, b, false);
+    checkRelate(a, b, "FF0FFF0F2");
+}
+
+//======= P/P  =============
+
+// testPointsContained
+template<>
+template<>
+void object::test<2> ()
+{
+    std::string a = "MULTIPOINT (0 0, 1 1, 2 2)";
+    std::string b = "MULTIPOINT (1 1, 2 2)";
+    checkIntersectsDisjoint(a, b, true);
+    checkContainsWithin(a, b, true);
+    checkEquals(a, b, false);
+    checkRelate(a, b, "0F0FFFFF2");
+}
+
+// testPointsEqual
+template<>
+template<>
+void object::test<3> ()
+{
+    std::string a = "MULTIPOINT (0 0, 1 1, 2 2)";
+    std::string b = "MULTIPOINT (0 0, 1 1, 2 2)";
+    checkIntersectsDisjoint(a, b, true);
+    checkContainsWithin(a, b, true);
+    checkEquals(a, b, true);
+}
+
+// testValidateRelatePP_13
+template<>
+template<>
+void object::test<4> ()
+{
+    std::string a = "MULTIPOINT ((80 70), (140 120), (20 20), (200 170))";
+    std::string b = "MULTIPOINT ((80 70), (140 120), (80 170), (200 80))";
+    checkIntersectsDisjoint(a, b, true);
+    checkContainsWithin(a, b, false);
+    checkContainsWithin(b, a, false);
+    checkCoversCoveredBy(a, b, false);
+    checkOverlaps(a, b, true);
+    checkTouches(a, b, false);
+}
+
+//======= L/P  =============
+
+// testLinePointContains
+template<>
+template<>
+void object::test<5> ()
+{
+    std::string a = "LINESTRING (0 0, 1 1, 2 2)";
+    std::string b = "MULTIPOINT (0 0, 1 1, 2 2)";
+    checkRelate(a, b, "0F10FFFF2");
+    checkIntersectsDisjoint(a, b, true);
+    checkContainsWithin(a, b, true);
+    checkContainsWithin(b, a, false);
+    checkCoversCoveredBy(a, b, true);
+    checkCoversCoveredBy(b, a, false);
+}
+
+// testLinePointOverlaps
+template<>
+template<>
+void object::test<6> ()
+{
+    std::string a = "LINESTRING (0 0, 1 1)";
+    std::string b = "MULTIPOINT (0 0, 1 1, 2 2)";
+    checkIntersectsDisjoint(a, b, true);
+    checkContainsWithin(a, b, false);
+    checkContainsWithin(b, a, false);
+    checkCoversCoveredBy(a, b, false);
+    checkCoversCoveredBy(b, a, false);
+}
+
+// testZeroLengthLinePoint
+template<>
+template<>
+void object::test<7> ()
+{
+    std::string a = "LINESTRING (0 0, 0 0)";
+    std::string b = "POINT (0 0)";
+    checkRelate(a, b, "0FFFFFFF2");
+    checkIntersectsDisjoint(a, b, true);
+    checkContainsWithin(a, b, true);
+    checkContainsWithin(b, a, true);
+    checkCoversCoveredBy(a, b, true);
+    checkCoversCoveredBy(b, a, true);
+    checkEquals(a, b, true);
+}
+
+// testZeroLengthLineLine
+template<>
+template<>
+void object::test<8> ()
+{
+    std::string a = "LINESTRING (10 10, 10 10, 10 10)";
+    std::string b = "LINESTRING (10 10, 10 10)";
+    checkRelate(a, b, "0FFFFFFF2");
+    checkIntersectsDisjoint(a, b, true);
+    checkContainsWithin(a, b, true);
+    checkContainsWithin(b, a, true);
+    checkCoversCoveredBy(a, b, true);
+    checkCoversCoveredBy(b, a, true);
+    checkEquals(a, b, true);
+}
+
+// tests bug involving checking for non-zero-length lines
+// testNonZeroLengthLinePoint
+template<>
+template<>
+void object::test<9> ()
+{
+    std::string a = "LINESTRING (0 0, 0 0, 9 9)";
+    std::string b = "POINT (1 1)";
+    checkRelate(a, b, "0F1FF0FF2");
+    checkIntersectsDisjoint(a, b, true);
+    checkContainsWithin(a, b, true);
+    checkContainsWithin(b, a, false);
+    checkCoversCoveredBy(a, b, true);
+    checkCoversCoveredBy(b, a, false);
+    checkEquals(a, b, false);
+}
+
+// testLinePointIntAndExt
+template<>
+template<>
+void object::test<10> ()
+{
+    std::string a = "MULTIPOINT((60 60), (100 100))";
+    std::string b = "LINESTRING(40 40, 80 80)";
+    checkRelate(a, b, "0F0FFF102");
+}
+
+//======= L/L  =============
+
+// testLinesCrossProper
+template<>
+template<>
+void object::test<11> ()
+{
+    std::string a = "LINESTRING (0 0, 9 9)";
+    std::string b = "LINESTRING(0 9, 9 0)";
+    checkIntersectsDisjoint(a, b, true);
+    checkContainsWithin(a, b, false);
+}
+
+// testLinesOverlap
+template<>
+template<>
+void object::test<12> ()
+{
+    std::string a = "LINESTRING (0 0, 5 5)";
+    std::string b = "LINESTRING(3 3, 9 9)";
+    checkIntersectsDisjoint(a, b, true);
+    checkTouches(a, b, false);
+    checkOverlaps(a, b, true);
+}
+
+// testLinesCrossVertex
+template<>
+template<>
+void object::test<13> ()
+{
+    std::string a = "LINESTRING (0 0, 8 8)";
+    std::string b = "LINESTRING(0 8, 4 4, 8 0)";
+    checkIntersectsDisjoint(a, b, true);
+}
+
+// testLinesTouchVertex
+template<>
+template<>
+void object::test<14> ()
+{
+    std::string a = "LINESTRING (0 0, 8 0)";
+    std::string b = "LINESTRING(0 8, 4 0, 8 8)";
+    checkIntersectsDisjoint(a, b, true);
+}
+
+// testLinesDisjointByEnvelope
+template<>
+template<>
+void object::test<15> ()
+{
+    std::string a = "LINESTRING (0 0, 9 9)";
+    std::string b = "LINESTRING(10 19, 19 10)";
+    checkIntersectsDisjoint(a, b, false);
+    checkContainsWithin(a, b, false);
+}
+
+// testLinesDisjoint
+template<>
+template<>
+void object::test<16> ()
+{
+    std::string a = "LINESTRING (0 0, 9 9)";
+    std::string b = "LINESTRING (4 2, 8 6)";
+    checkIntersectsDisjoint(a, b, false);
+    checkContainsWithin(a, b, false);
+}
+
+// testLinesClosedEmpty
+template<>
+template<>
+void object::test<17> ()
+{
+    std::string a = "MULTILINESTRING ((0 0, 0 1), (0 1, 1 1, 1 0, 0 0))";
+    std::string b = "LINESTRING EMPTY";
+    checkRelate(a, b, "FF1FFFFF2");
+    checkIntersectsDisjoint(a, b, false);
+    checkContainsWithin(a, b, false);
+}
+
+// testLinesRingTouchAtNode
+template<>
+template<>
+void object::test<18> ()
+{
+    std::string a = "LINESTRING (5 5, 1 8, 1 1, 5 5)";
+    std::string b = "LINESTRING (5 5, 9 5)";
+    checkRelate(a, b, "F01FFF102");
+    checkIntersectsDisjoint(a, b, true);
+    checkContainsWithin(a, b, false);
+    checkTouches(a, b, true);
+}
+
+// testLinesTouchAtBdy
+template<>
+template<>
+void object::test<19> ()
+{
+    std::string a = "LINESTRING (5 5, 1 8)";
+    std::string b = "LINESTRING (5 5, 9 5)";
+    checkRelate(a, b, "FF1F00102");
+    checkIntersectsDisjoint(a, b, true);
+    checkContainsWithin(a, b, false);
+    checkTouches(a, b, true);
+}
+
+// testLinesOverlapWithDisjointLine
+template<>
+template<>
+void object::test<20> ()
+{
+    std::string a = "LINESTRING (1 1, 9 9)";
+    std::string b = "MULTILINESTRING ((2 2, 8 8), (6 2, 8 4))";
+    checkRelate(a, b, "101FF0102");
+    checkIntersectsDisjoint(a, b, true);
+    checkContainsWithin(a, b, false);
+    checkOverlaps(a, b, true);
+}
+
+// testLinesDisjointOverlappingEnvelopes
+template<>
+template<>
+void object::test<21> ()
+{
+    std::string a = "LINESTRING (60 0, 20 80, 100 80, 80 120, 40 140)";
+    std::string b = "LINESTRING (60 40, 140 40, 140 160, 0 160)";
+    checkRelate(a, b, "FF1FF0102");
+    checkIntersectsDisjoint(a, b, false);
+    checkContainsWithin(a, b, false);
+    checkTouches(a, b, false);
+}
+
+/**
+* Case from https://github.com/locationtech/jts/issues/270
+* Strictly, the lines cross, since their interiors intersect
+* according to the Orientation predicate.
+* However, the computation of the intersection point is
+* non-robust, and reports it as being equal to the endpoint
+* POINT (-10 0.0000000000000012)
+* For consistency the relate algorithm uses the intersection node topology.
+*/
+// testLinesCross_JTS270
+template<>
+template<>
+void object::test<22> ()
+{
+    std::string a = "LINESTRING (0 0, -10 0.0000000000000012)";
+    std::string b = "LINESTRING (-9.999143275740073 -0.1308959557133398, -10 0.0000000000001054)";
+    checkIntersectsDisjoint(a, b, true);
+    checkContainsWithin(a, b, false);
+    checkCoversCoveredBy(a, b, false);
+    checkCrosses(a, b, false);
+    checkOverlaps(a, b, false);
+    checkTouches(a, b, true);
+}
+
+// testLinesContained_JTS396
+template<>
+template<>
+void object::test<23> ()
+{
+    std::string a = "LINESTRING (1 0, 0 2, 0 0, 2 2)";
+    std::string b = "LINESTRING (0 0, 2 2)";
+    checkIntersectsDisjoint(a, b, true);
+    checkContainsWithin(a, b, true);
+    checkCoversCoveredBy(a, b, true);
+    checkCrosses(a, b, false);
+    checkOverlaps(a, b, false);
+    checkTouches(a, b, false);
+}
+
+
+/**
+* This case shows that lines must be self-noded,
+* so that node topology is constructed correctly
+* (at least for some predicates).
+*/
+// testLinesContainedWithSelfIntersection
+template<>
+template<>
+void object::test<24> ()
+{
+    std::string a = "LINESTRING (2 0, 0 2, 0 0, 2 2)";
+    std::string b = "LINESTRING (0 0, 2 2)";
+  //checkIntersectsDisjoint(a, b, true);
+    checkContainsWithin(a, b, true);
+    checkCoversCoveredBy(a, b, true);
+    checkCrosses(a, b, false);
+    checkOverlaps(a, b, false);
+    checkTouches(a, b, false);
+}
+
+// testLineContainedInRing
+template<>
+template<>
+void object::test<25> ()
+{
+    std::string a = "LINESTRING(60 60, 100 100, 140 60)";
+    std::string b = "LINESTRING(100 100, 180 20, 20 20, 100 100)";
+    checkIntersectsDisjoint(a, b, true);
+    checkContainsWithin(b, a, true);
+    checkCoversCoveredBy(b, a, true);
+    checkCrosses(a, b, false);
+    checkOverlaps(a, b, false);
+    checkTouches(a, b, false);
+}
+
+// see https://github.com/libgeos/geos/issues/933
+// testLineLineProperIntersection
+template<>
+template<>
+void object::test<26> ()
+{
+    std::string a = "MULTILINESTRING ((0 0, 1 1), (0.5 0.5, 1 0.1, -1 0.1))";
+    std::string b = "LINESTRING (0 0, 1 1)";
+  //checkIntersectsDisjoint(a, b, true);
+    checkContainsWithin(a, b, true);
+    checkCoversCoveredBy(a, b, true);
+    checkCrosses(a, b, false);
+    checkOverlaps(a, b, false);
+    checkTouches(a, b, false);
+}
+
+// testLineSelfIntersectionCollinear
+template<>
+template<>
+void object::test<27> ()
+{
+    std::string a = "LINESTRING (9 6, 1 6, 1 0, 5 6, 9 6)";
+    std::string b = "LINESTRING (9 9, 3 1)";
+    checkRelate(a, b, "0F1FFF102");
+}
+
+//======= A/P  =============
+
+// testPolygonPointInside
+template<>
+template<>
+void object::test<28> ()
+{
+    std::string a = "POLYGON ((0 10, 10 10, 10 0, 0 0, 0 10))";
+    std::string b = "POINT (1 1)";
+    checkIntersectsDisjoint(a, b, true);
+    checkContainsWithin(a, b, true);
+}
+
+// testPolygonPointOutside
+template<>
+template<>
+void object::test<29> ()
+{
+    std::string a = "POLYGON ((10 0, 0 0, 0 10, 10 0))";
+    std::string b = "POINT (8 8)";
+    checkIntersectsDisjoint(a, b, false);
+    checkContainsWithin(a, b, false);
+}
+
+// testPolygonPointInBoundary
+template<>
+template<>
+void object::test<30> ()
+{
+    std::string a = "POLYGON ((10 0, 0 0, 0 10, 10 0))";
+    std::string b = "POINT (1 0)";
+    checkIntersectsDisjoint(a, b, true);
+    checkContainsWithin(a, b, false);
+    checkCoversCoveredBy(a, b, true);
+}
+
+// testAreaPointInExterior
+template<>
+template<>
+void object::test<31> ()
+{
+    std::string a = "POLYGON ((1 5, 5 5, 5 1, 1 1, 1 5))";
+    std::string b = "POINT (7 7)";
+    checkRelate(a, b, "FF2FF10F2");
+    checkIntersectsDisjoint(a, b, false);
+    checkContainsWithin(a, b, false);
+    checkCoversCoveredBy(a, b, false);
+    checkTouches(a, b, false);
+    checkOverlaps(a, b, false);
+}
+
+//======= A/L  =============
+
+
+// testAreaLineContainedAtLineVertex
+template<>
+template<>
+void object::test<32> ()
+{
+    std::string a = "POLYGON ((1 5, 5 5, 5 1, 1 1, 1 5))";
+    std::string b = "LINESTRING (2 3, 3 5, 4 3)";
+    checkIntersectsDisjoint(a, b, true);
+    checkTouches(a, b, false);
+    checkOverlaps(a, b, false);
+}
+
+// testAreaLineTouchAtLineVertex
+template<>
+template<>
+void object::test<33> ()
+{
+    std::string a = "POLYGON ((1 5, 5 5, 5 1, 1 1, 1 5))";
+    std::string b = "LINESTRING (1 8, 3 5, 5 8)";
+    checkIntersectsDisjoint(a, b, true);
+    checkContainsWithin(a, b, false);
+    checkCoversCoveredBy(a, b, false);
+    checkTouches(a, b, true);
+    checkOverlaps(a, b, false);
+}
+
+// testPolygonLineInside
+template<>
+template<>
+void object::test<34> ()
+{
+    std::string a = "POLYGON ((0 10, 10 10, 10 0, 0 0, 0 10))";
+    std::string b = "LINESTRING (1 8, 3 5, 5 8)";
+    checkRelate(a, b, "102FF1FF2");
+    checkIntersectsDisjoint(a, b, true);
+    checkContainsWithin(a, b, true);
+}
+
+// testPolygonLineOutside
+template<>
+template<>
+void object::test<35> ()
+{
+    std::string a = "POLYGON ((10 0, 0 0, 0 10, 10 0))";
+    std::string b = "LINESTRING (4 8, 9 3)";
+    checkIntersectsDisjoint(a, b, false);
+    checkContainsWithin(a, b, false);
+}
+
+// testPolygonLineInBoundary
+template<>
+template<>
+void object::test<36> ()
+{
+    std::string a = "POLYGON ((10 0, 0 0, 0 10, 10 0))";
+    std::string b = "LINESTRING (1 0, 9 0)";
+    checkIntersectsDisjoint(a, b, true);
+    checkContainsWithin(a, b, false);
+    checkCoversCoveredBy(a, b, true);
+    checkTouches(a, b, true);
+    checkOverlaps(a, b, false);
+}
+
+// testPolygonLineCrossingContained
+template<>
+template<>
+void object::test<37> ()
+{
+    std::string a = "MULTIPOLYGON (((20 80, 180 80, 100 0, 20 80)), ((20 160, 180 160, 100 80, 20 160)))";
+    std::string b = "LINESTRING (100 140, 100 40)";
+    checkRelate(a, b, "1020F1FF2");
+    checkIntersectsDisjoint(a, b, true);
+    checkContainsWithin(a, b, true);
+    checkCoversCoveredBy(a, b, true);
+    checkTouches(a, b, false);
+    checkOverlaps(a, b, false);
+}
+
+// testValidateRelateLA_220
+template<>
+template<>
+void object::test<38> ()
+{
+    std::string a = "LINESTRING (90 210, 210 90)";
+    std::string b = "POLYGON ((150 150, 410 150, 280 20, 20 20, 150 150))";
+    checkIntersectsDisjoint(a, b, true);
+    checkContainsWithin(a, b, false);
+    checkCoversCoveredBy(a, b, false);
+    checkTouches(a, b, false);
+    checkOverlaps(a, b, false);
+}
+
+  /**
+   * See RelateLA.xml (line 585)
+   */
+// testLineCrossingPolygonAtShellHolePoint
+template<>
+template<>
+void object::test<39> ()
+{
+    std::string a = "LINESTRING (60 160, 150 70)";
+    std::string b = "POLYGON ((190 190, 360 20, 20 20, 190 190), (110 110, 250 100, 140 30, 110 110))";
+    checkRelate(a, b, "F01FF0212");
+    checkTouches(a, b, true);
+    checkIntersectsDisjoint(a, b, true);
+    checkContainsWithin(a, b, false);
+    checkCoversCoveredBy(a, b, false);
+    checkTouches(a, b, true);
+    checkOverlaps(a, b, false);
+}
+
+// testLineCrossingPolygonAtNonVertex
+template<>
+template<>
+void object::test<40> ()
+{
+    std::string a = "LINESTRING (20 60, 150 60)";
+    std::string b = "POLYGON ((150 150, 410 150, 280 20, 20 20, 150 150))";
+    checkIntersectsDisjoint(a, b, true);
+    checkContainsWithin(a, b, false);
+    checkCoversCoveredBy(a, b, false);
+    checkTouches(a, b, false);
+    checkOverlaps(a, b, false);
+}
+
+// testPolygonLinesContainedCollinearEdge
+template<>
+template<>
+void object::test<41> ()
+{
+    std::string a = "POLYGON ((110 110, 200 20, 20 20, 110 110))";
+    std::string b = "MULTILINESTRING ((110 110, 60 40, 70 20, 150 20, 170 40), (180 30, 40 30, 110 80))";
+    checkRelate(a, b, "102101FF2");
+}
+
+//======= A/A  =============
+
+
+// testPolygonsEdgeAdjacent
+template<>
+template<>
+void object::test<42> ()
+{
+    std::string a = "POLYGON ((1 3, 3 3, 3 1, 1 1, 1 3))";
+    std::string b = "POLYGON ((5 3, 5 1, 3 1, 3 3, 5 3))";
+  //checkIntersectsDisjoint(a, b, true);
+    checkOverlaps(a, b, false);
+    checkTouches(a, b, true);
+}
+
+// testPolygonsEdgeAdjacent2
+template<>
+template<>
+void object::test<43> ()
+{
+    std::string a = "POLYGON ((1 3, 4 3, 3 0, 1 1, 1 3))";
+    std::string b = "POLYGON ((5 3, 5 1, 3 0, 4 3, 5 3))";
+  //checkIntersectsDisjoint(a, b, true);
+    checkOverlaps(a, b, false);
+    checkTouches(a, b, true);
+}
+
+// testPolygonsNested
+template<>
+template<>
+void object::test<44> ()
+{
+    std::string a = "POLYGON ((1 9, 9 9, 9 1, 1 1, 1 9))";
+    std::string b = "POLYGON ((2 8, 8 8, 8 2, 2 2, 2 8))";
+    checkIntersectsDisjoint(a, b, true);
+    checkContainsWithin(a, b, true);
+    checkCoversCoveredBy(a, b, true);
+    checkOverlaps(a, b, false);
+    checkTouches(a, b, false);
+}
+
+// testPolygonsOverlapProper
+template<>
+template<>
+void object::test<45> ()
+{
+    std::string a = "POLYGON ((1 1, 1 7, 7 7, 7 1, 1 1))";
+    std::string b = "POLYGON ((2 8, 8 8, 8 2, 2 2, 2 8))";
+    checkIntersectsDisjoint(a, b, true);
+    checkContainsWithin(a, b, false);
+    checkCoversCoveredBy(a, b, false);
+    checkOverlaps(a, b, true);
+    checkTouches(a, b, false);
+}
+
+// testPolygonsOverlapAtNodes
+template<>
+template<>
+void object::test<46> ()
+{
+    std::string a = "POLYGON ((1 5, 5 5, 5 1, 1 1, 1 5))";
+    std::string b = "POLYGON ((7 3, 5 1, 3 3, 5 5, 7 3))";
+    checkIntersectsDisjoint(a, b, true);
+    checkContainsWithin(a, b, false);
+    checkCoversCoveredBy(a, b, false);
+    checkOverlaps(a, b, true);
+    checkTouches(a, b, false);
+}
+
+// testPolygonsContainedAtNodes
+template<>
+template<>
+void object::test<47> ()
+{
+    std::string a = "POLYGON ((1 5, 5 5, 6 2, 1 1, 1 5))";
+    std::string b = "POLYGON ((1 1, 5 5, 6 2, 1 1))";
+  //checkIntersectsDisjoint(a, b, true);
+    checkContainsWithin(a, b, true);
+    checkCoversCoveredBy(a, b, true);
+    checkOverlaps(a, b, false);
+    checkTouches(a, b, false);
+}
+
+// testPolygonsNestedWithHole
+template<>
+template<>
+void object::test<48> ()
+{
+    std::string a = "POLYGON ((40 60, 420 60, 420 320, 40 320, 40 60), (200 140, 160 220, 260 200, 200 140))";
+    std::string b = "POLYGON ((80 100, 360 100, 360 280, 80 280, 80 100))";
+  //checkIntersectsDisjoint(true, a, b);
+    checkContainsWithin(a, b, false);
+    checkContainsWithin(b, a, false);
+  //checkCoversCoveredBy(false, a, b);
+  //checkOverlaps(true, a, b);
+    checkPredicate(*RelatePredicate::contains(), a, b, false);
+  //checkTouches(false, a, b);
+}
+
+// testPolygonsOverlappingWithBoundaryInside
+template<>
+template<>
+void object::test<49> ()
+{
+    std::string a = "POLYGON ((100 60, 140 100, 100 140, 60 100, 100 60))";
+    std::string b = "MULTIPOLYGON (((80 40, 120 40, 120 80, 80 80, 80 40)), ((120 80, 160 80, 160 120, 120 120, 120 80)), ((80 120, 120 120, 120 160, 80 160, 80 120)), ((40 80, 80 80, 80 120, 40 120, 40 80)))";
+    checkRelate(a, b, "21210F212");
+    checkIntersectsDisjoint(a, b, true);
+    checkContainsWithin(a, b, false);
+    checkContainsWithin(b, a, false);
+    checkCoversCoveredBy(a, b, false);
+    checkOverlaps(a, b, true);
+    checkTouches(a, b, false);
+}
+
+// testPolygonsOverlapVeryNarrow
+template<>
+template<>
+void object::test<50> ()
+{
+    std::string a = "POLYGON ((120 100, 120 200, 200 200, 200 100, 120 100))";
+    std::string b = "POLYGON ((100 100, 100000 110, 100000 100, 100 100))";
+    checkRelate(a, b, "212111212");
+    checkIntersectsDisjoint(a, b, true);
+    checkContainsWithin(a, b, false);
+    checkContainsWithin(b, a, false);
+  //checkCoversCoveredBy(false, a, b);
+  //checkOverlaps(true, a, b);
+  //checkTouches(false, a, b);
+}
+
+// testValidateRelateAA_86
+template<>
+template<>
+void object::test<51> ()
+{
+    std::string a = "POLYGON ((170 120, 300 120, 250 70, 120 70, 170 120))";
+    std::string b = "POLYGON ((150 150, 410 150, 280 20, 20 20, 150 150), (170 120, 330 120, 260 50, 100 50, 170 120))";
+    checkIntersectsDisjoint(a, b, true);
+    checkContainsWithin(a, b, false);
+    checkCoversCoveredBy(a, b, false);
+    checkOverlaps(a, b, false);
+    checkPredicate(*RelatePredicate::within(), a, b, false);
+    checkTouches(a, b, true);
+}
+
+// testValidateRelateAA_97
+template<>
+template<>
+void object::test<52> ()
+{
+    std::string a = "POLYGON ((330 150, 200 110, 150 150, 280 190, 330 150))";
+    std::string b = "MULTIPOLYGON (((140 110, 260 110, 170 20, 50 20, 140 110)), ((300 270, 420 270, 340 190, 220 190, 300 270)))";
+    checkIntersectsDisjoint(a, b, true);
+    checkContainsWithin(a, b, false);
+    checkCoversCoveredBy(a, b, false);
+    checkOverlaps(a, b, false);
+    checkPredicate(*RelatePredicate::within(), a, b, false);
+    checkTouches(a, b, true);
+}
+
+// testAdjacentPolygons
+template<>
+template<>
+void object::test<53> ()
+{
+    std::string a = "POLYGON ((1 9, 6 9, 6 1, 1 1, 1 9))";
+    std::string b = "POLYGON ((9 9, 9 4, 6 4, 6 9, 9 9))";
+    checkRelateMatches(a, b, IntersectionMatrixPattern::ADJACENT, true);
+}
+
+// testAdjacentPolygonsTouchingAtPoint
+template<>
+template<>
+void object::test<54> ()
+{
+    std::string a = "POLYGON ((1 9, 6 9, 6 1, 1 1, 1 9))";
+    std::string b = "POLYGON ((9 9, 9 4, 6 4, 7 9, 9 9))";
+    checkRelateMatches(a, b, IntersectionMatrixPattern::ADJACENT, false);
+}
+
+// testAdjacentPolygonsOverlappping
+template<>
+template<>
+void object::test<55> ()
+{
+    std::string a = "POLYGON ((1 9, 6 9, 6 1, 1 1, 1 9))";
+    std::string b = "POLYGON ((9 9, 9 4, 6 4, 5 9, 9 9))";
+    checkRelateMatches(a, b, IntersectionMatrixPattern::ADJACENT, false);
+}
+
+// testContainsProperlyPolygonContained
+template<>
+template<>
+void object::test<56> ()
+{
+    std::string a = "POLYGON ((1 9, 9 9, 9 1, 1 1, 1 9))";
+    std::string b = "POLYGON ((2 8, 5 8, 5 5, 2 5, 2 8))";
+    checkRelateMatches(a, b, IntersectionMatrixPattern::CONTAINS_PROPERLY, true);
+}
+
+// testContainsProperlyPolygonTouching
+template<>
+template<>
+void object::test<57> ()
+{
+    std::string a = "POLYGON ((1 9, 9 9, 9 1, 1 1, 1 9))";
+    std::string b = "POLYGON ((9 1, 5 1, 5 5, 9 5, 9 1))";
+    checkRelateMatches(a, b, IntersectionMatrixPattern::CONTAINS_PROPERLY, false);
+}
+
+// testContainsProperlyPolygonsOverlapping
+template<>
+template<>
+void object::test<58> ()
+{
+    std::string a = "GEOMETRYCOLLECTION (POLYGON ((1 9, 6 9, 6 4, 1 4, 1 9)), POLYGON ((2 4, 6 7, 9 1, 2 4)))";
+    std::string b = "POLYGON ((5 5, 6 5, 6 4, 5 4, 5 5))";
+    checkRelateMatches(a, b, IntersectionMatrixPattern::CONTAINS_PROPERLY, true);
+}
+
+//================  Repeated Points  ==============
+
+// testRepeatedPointLL
+template<>
+template<>
+void object::test<59> ()
+{
+    std::string a = "LINESTRING(0 0, 5 5, 5 5, 5 5, 9 9)";
+    std::string b = "LINESTRING(0 9, 5 5, 5 5, 5 5, 9 0)";
+    checkRelate(a, b, "0F1FF0102");
+    checkIntersectsDisjoint(a, b, true);
+}
+
+// testRepeatedPointAA
+template<>
+template<>
+void object::test<60> ()
+{
+    std::string a = "POLYGON ((1 9, 9 7, 9 1, 1 3, 1 9))";
+    std::string b = "POLYGON ((1 3, 1 3, 1 3, 3 7, 9 7, 9 7, 1 3))";
+    checkRelate(a, b, "212F01FF2");
+}
+
+//================  Empty Geometries  ==============
+
+//-- test equals against all combinations of empty geometries
+template<>
+template<>
+void object::test<61> ()
+{
+    std::string empties[] = {
+        "POINT EMPTY",
+        "LINESTRING EMPTY",
+        "POLYGON EMPTY",
+        "MULTIPOINT EMPTY",
+        "MULTILINESTRING EMPTY",
+        "MULTIPOLYGON EMPTY",
+        "GEOMETRYCOLLECTION EMPTY"
+    };
+    int nempty = 7;
+    for (int i = 0; i < nempty; i++) {
+        for (int j = 0; j < nempty; j++) {
+            std::string a = empties[i];
+            std::string b = empties[j];
+            checkRelate(a, b, "FFFFFFFF2");
+            checkEquals(a, b, true);
+        }
+    }
+
+}
+
+// Prepared test
+template<>
+template<>
+void object::test<62> ()
+{
+    std::string a = "POLYGON((0 0, 1 0, 1 1, 0 1, 0 0))";
+    std::string b = "POLYGON((0.5 0.5, 1.5 0.5, 1.5 1.5, 0.5 1.5, 0.5 0.5))";
+    checkRelate(a, b, "212101212");
+}
+
+
+} // namespace tut
diff --git a/tests/unit/operation/relateng/RelateNGTest.h b/tests/unit/operation/relateng/RelateNGTest.h
new file mode 100644
index 000000000..70526d6de
--- /dev/null
+++ b/tests/unit/operation/relateng/RelateNGTest.h
@@ -0,0 +1,134 @@
+
+// geos
+#include <geos/io/WKTReader.h>
+#include <geos/io/WKTWriter.h>
+#include <geos/geom/Geometry.h>
+#include <geos/operation/relateng/RelateNG.h>
+#include <geos/operation/relateng/RelatePredicate.h>
+#include <geos/operation/relateng/RelateMatrixPredicate.h>
+#include <geos/operation/relateng/IntersectionMatrixPattern.h>
+
+// std
+#include <memory>
+
+using namespace geos::geom;
+using namespace geos::operation::relateng;
+using geos::io::WKTReader;
+using geos::io::WKTWriter;
+
+namespace tut {
+//
+// Test Group
+//
+
+// Common data used by all tests
+struct test_relateng_support {
+
+    WKTReader r;
+    WKTWriter w;
+
+    void checkPrepared(const std::string& wkta, const std::string& wktb)
+    {
+        std::unique_ptr<Geometry> a = r.read(wkta);
+        std::unique_ptr<Geometry> b = r.read(wktb);
+        return checkPrepared(a.get(), b.get());
+    }
+
+    void checkPrepared(const Geometry* a, const Geometry* b)
+    {
+        auto prep_a = RelateNG::prepare(a);
+        auto prep_b = RelateNG::prepare(b);
+        ensure_equals("preparedEqualsTopo", prep_a->equalsTopo(b), a->equals(b));
+        ensure_equals("preparedIntersects", prep_a->intersects(b), a->intersects(b));
+        ensure_equals("preparedDisjoint",   prep_a->disjoint(b),   a->disjoint(b));
+        ensure_equals("preparedCovers",     prep_a->covers(b),     a->covers(b));
+        ensure_equals("preparedCoveredby",  prep_a->coveredBy(b),  a->coveredBy(b));
+        ensure_equals("preparedWithin",     prep_a->within(b),     a->within(b));
+        ensure_equals("preparedContains",   prep_a->contains(b),   a->contains(b));
+        ensure_equals("preparedCrosses",    prep_a->crosses(b),    a->crosses(b));
+        ensure_equals("preparedTouches",    prep_a->touches(b),    a->touches(b));
+        ensure_equals("preparedRelate", prep_a->relate(b)->toString(), a->relate(b)->toString());
+    }
+
+    void checkIntersectsDisjoint(const std::string& wkta, const std::string& wktb, bool expectedValue)
+    {
+        checkPredicate(*RelatePredicate::intersects(), wkta, wktb, expectedValue);
+        checkPredicate(*RelatePredicate::intersects(), wktb, wkta, expectedValue);
+        checkPredicate(*RelatePredicate::disjoint(), wkta, wktb, ! expectedValue);
+        checkPredicate(*RelatePredicate::disjoint(), wktb, wkta, ! expectedValue);
+    }
+
+    void checkContainsWithin(const std::string& wkta, const std::string& wktb, bool expectedValue)
+    {
+        checkPredicate(*RelatePredicate::contains(), wkta, wktb, expectedValue);
+        checkPredicate(*RelatePredicate::within(),   wktb, wkta, expectedValue);
+    }
+
+    void checkCoversCoveredBy(const std::string& wkta, const std::string& wktb, bool expectedValue)
+    {
+        checkPredicate(*RelatePredicate::covers(),    wkta, wktb, expectedValue);
+        checkPredicate(*RelatePredicate::coveredBy(), wktb, wkta, expectedValue);
+    }
+
+    void checkCrosses(const std::string& wkta, const std::string& wktb, bool expectedValue)
+    {
+        checkPredicate(*RelatePredicate::crosses(), wkta, wktb, expectedValue);
+        checkPredicate(*RelatePredicate::crosses(), wktb, wkta, expectedValue);
+    }
+
+    void checkOverlaps(const std::string& wkta, const std::string& wktb, bool expectedValue)
+    {
+        checkPredicate(*RelatePredicate::overlaps(), wkta, wktb, expectedValue);
+        checkPredicate(*RelatePredicate::overlaps(), wktb, wkta, expectedValue);
+    }
+
+    void checkTouches(const std::string& wkta, const std::string& wktb, bool expectedValue)
+    {
+        checkPredicate(*RelatePredicate::touches(), wkta, wktb, expectedValue);
+        checkPredicate(*RelatePredicate::touches(), wktb, wkta, expectedValue);
+    }
+
+    void checkEquals(const std::string& wkta, const std::string& wktb, bool expectedValue)
+    {
+        checkPredicate(*RelatePredicate::equalsTopo(), wkta, wktb, expectedValue);
+        checkPredicate(*RelatePredicate::equalsTopo(), wktb, wkta, expectedValue);
+    }
+
+    void checkRelate(const std::string& wkta, const std::string& wktb, const std::string expectedValue)
+    {
+        std::unique_ptr<Geometry> a = r.read(wkta);
+        std::unique_ptr<Geometry> b = r.read(wktb);
+        RelateMatrixPredicate pred;
+        // TopologyPredicate predTrace = trace(pred);
+        RelateNG::relate(a.get(), b.get(), pred);
+        std::string actualVal = pred.getIM()->toString();
+        if (actualVal != expectedValue) {
+            std::cerr << std::endl << w.write(*a) << " relate " << w.write(*b) << " = " << actualVal << std::endl;
+        }
+        ensure_equals("checkRelate", actualVal, expectedValue);
+        checkPrepared(a.get(), b.get());
+    }
+
+    void checkRelateMatches(const std::string& wkta, const std::string& wktb, const std::string pattern, bool expectedValue)
+    {
+        auto pred = RelatePredicate::matches(pattern);
+        checkPredicate(*pred, wkta, wktb, expectedValue);
+    }
+
+    void checkPredicate(TopologyPredicate& pred, const std::string& wkta, const std::string& wktb, bool expectedValue)
+    {
+        std::unique_ptr<Geometry> a = r.read(wkta);
+        std::unique_ptr<Geometry> b = r.read(wktb);
+        // TopologyPredicate predTrace = trace(pred);
+        bool actualVal = RelateNG::relate(a.get(), b.get(), pred);
+        if (actualVal != expectedValue) {
+            std::cerr << std::endl << w.write(*a) << " " << pred << " " << w.write(*b) << " = " << actualVal << std::endl;
+        }
+        ensure_equals("checkPredicate", actualVal, expectedValue);
+        checkPrepared(a.get(), b.get());
+    }
+
+};
+
+
+} // namespace tut
diff --git a/tests/unit/operation/relateng/RelatePointLocatorTest.cpp b/tests/unit/operation/relateng/RelatePointLocatorTest.cpp
new file mode 100644
index 000000000..5947216bf
--- /dev/null
+++ b/tests/unit/operation/relateng/RelatePointLocatorTest.cpp
@@ -0,0 +1,149 @@
+//
+// Test Suite for geos::operation::relateng::RelatePointLocator class.
+
+#include <tut/tut.hpp>
+#include <utility.h>
+
+// geos
+#include <geos/io/WKTReader.h>
+#include <geos/io/WKTWriter.h>
+#include <geos/geom/Geometry.h>
+#include <geos/geom/Location.h>
+#include <geos/operation/relateng/RelatePointLocator.h>
+#include <geos/operation/relateng/DimensionLocation.h>
+
+// std
+#include <memory>
+
+using namespace geos::geom;
+using namespace geos::operation::relateng;
+using geos::io::WKTReader;
+// using geos::io::WKTWriter;
+
+namespace tut {
+//
+// Test Group
+//
+
+// Common data used by all tests
+struct test_relatepointlocator_data {
+
+    WKTReader r;
+    // WKTWriter w;
+
+    std::string gcPLA =
+        "GEOMETRYCOLLECTION (POINT (1 1), POINT (2 1), LINESTRING (3 1, 3 9), LINESTRING (4 1, 5 4, 7 1, 4 1), LINESTRING (12 12, 14 14), POLYGON ((6 5, 6 9, 9 9, 9 5, 6 5)), POLYGON ((10 10, 10 16, 16 16, 16 10, 10 10)), POLYGON ((11 11, 11 17, 17 17, 17 11, 11 11)), POLYGON ((12 12, 12 16, 16 16, 16 12, 12 12)))";
+
+    // void
+    // checkLocation(const std::string& wkt, int x, int y, Location expectedLoc)
+    // {
+    //     std::unique_ptr<Geometry> geom = r.read(wkt);
+    //     RelatePointLocator ael(geom.get());
+    //     Coordinate c(x, y);
+    //     Location loc = ael.locate(&c);
+    //     ensure_equals("Locations are not equal ", expectedLoc, loc);
+    // }
+
+
+    void checkDimLocation(const std::string& wkt, double i, double j, int expected)
+    {
+        std::unique_ptr<Geometry> geom = r.read(wkt);
+        RelatePointLocator locator(geom.get());
+        CoordinateXY c(i, j);
+        int actual = locator.locateWithDim(&c);
+        ensure_equals("checkLocation", expected, actual);
+    }
+
+    void checkNodeLocation(const std::string& wkt, double i, double j, Location expected)
+    {
+        std::unique_ptr<Geometry> geom = r.read(wkt);
+        RelatePointLocator locator(geom.get());
+        CoordinateXY c(i, j);
+        Location actual = locator.locateNode(&c, nullptr);
+        ensure_equals("checkNodeLocation", expected, actual);
+    }
+
+};
+
+typedef test_group<test_relatepointlocator_data> group;
+typedef group::object object;
+
+group test_relatepointlocator_group("geos::operation::relateng::RelatePointLocator");
+
+
+// testPoint
+template<>
+template<>
+void object::test<1> ()
+{
+    //std::string wkt("GEOMETRYCOLLECTION (POINT(0 0), POINT(1 1))");
+    checkDimLocation(gcPLA, 1, 1, DimensionLocation::POINT_INTERIOR);
+    checkDimLocation(gcPLA, 0, 1, DimensionLocation::EXTERIOR);
+}
+
+// testPointInLine
+template<>
+template<>
+void object::test<2> ()
+{
+    checkDimLocation(gcPLA, 3, 8, DimensionLocation::LINE_INTERIOR);
+}
+
+// testPointInArea
+template<>
+template<>
+void object::test<3> ()
+{
+    checkDimLocation(gcPLA, 8, 8, DimensionLocation::AREA_INTERIOR);
+}
+
+// testLine
+template<>
+template<>
+void object::test<4> ()
+{
+    checkDimLocation(gcPLA, 3, 3, DimensionLocation::LINE_INTERIOR);
+    checkDimLocation(gcPLA, 3, 1, DimensionLocation::LINE_BOUNDARY);
+}
+
+// testLineInArea
+template<>
+template<>
+void object::test<5> ()
+{
+    checkDimLocation(gcPLA, 11, 11, DimensionLocation::AREA_INTERIOR);
+    checkDimLocation(gcPLA, 14, 14, DimensionLocation::AREA_INTERIOR);
+}
+
+// testArea
+template<>
+template<>
+void object::test<6> ()
+{
+    checkDimLocation(gcPLA, 8, 8, DimensionLocation::AREA_INTERIOR);
+    checkDimLocation(gcPLA, 9, 9, DimensionLocation::AREA_BOUNDARY);
+}
+
+// testAreaInArea
+template<>
+template<>
+void object::test<7> ()
+{
+    checkDimLocation(gcPLA, 11, 11, DimensionLocation::AREA_INTERIOR);
+    checkDimLocation(gcPLA, 12, 12, DimensionLocation::AREA_INTERIOR);
+    checkDimLocation(gcPLA, 10, 10, DimensionLocation::AREA_BOUNDARY);
+    checkDimLocation(gcPLA, 16, 16, DimensionLocation::AREA_INTERIOR);
+}
+
+// testLineNode
+template<>
+template<>
+void object::test<8> ()
+{
+    //checkNodeLocation(gcPLA, 12.1, 12.2, Location::INTERIOR);
+    checkNodeLocation(gcPLA, 3, 1, Location::BOUNDARY);
+}
+
+
+
+} // namespace tut
diff --git a/tests/unit/operation/relateng/RelatePredicateTest.cpp b/tests/unit/operation/relateng/RelatePredicateTest.cpp
new file mode 100644
index 000000000..7408f3275
--- /dev/null
+++ b/tests/unit/operation/relateng/RelatePredicateTest.cpp
@@ -0,0 +1,138 @@
+//
+// Test Suite for geos::operation::relateng::LinearBoundary class.
+
+#include <tut/tut.hpp>
+#include <utility.h>
+
+// geos
+#include <geos/geom/Location.h>
+#include <geos/geom/Dimension.h>
+#include <geos/operation/relateng/TopologyPredicate.h>
+#include <geos/operation/relateng/RelatePredicate.h>
+
+// std
+#include <memory>
+
+
+using namespace geos::geom;
+using namespace geos::operation::relateng;
+
+
+namespace tut {
+//
+// Test Group
+//
+
+// Common data used by all tests
+struct test_relatepredicate_data {
+
+    void
+    checkPredicate(TopologyPredicate& pred, const std::string& im, bool expected)
+    {
+        applyIM(im, pred);
+        checkPred(pred, expected);
+    }
+
+    void
+    checkPredicatePartial(TopologyPredicate& pred, const std::string& im, bool expected)
+    {
+        applyIM(im, pred);
+        bool isKnown = pred.isKnown();
+        ensure("predicate value is not known", isKnown);
+        checkPred(pred, expected);
+    }
+
+    void
+    checkPred(TopologyPredicate& pred, bool expected)
+    {
+        pred.finish();
+        bool actual = pred.value();
+        ensure_equals(expected, actual);
+    }
+
+    static void
+    applyIM(const std::string& imIn, TopologyPredicate& pred)
+    {
+        std::array<Location,3> locs = {
+            Location::INTERIOR, Location::BOUNDARY, Location::EXTERIOR };
+
+        const std::string& im = cleanIM(imIn);
+        uint32_t i = 0;
+        for (Location locA : locs) {
+            for (Location locB : locs) {
+                char entry = im[i++];
+                if (entry == '0' || entry == '1' || entry == '2') {
+                    int dim = Dimension::toDimensionValue(entry);
+                    pred.updateDimension(locA, locB, dim);
+                }
+            }
+        }
+    }
+
+    static std::string
+    cleanIM(const std::string& im)
+    {
+        std::string str = im;
+        str.erase(std::remove(str.begin(), str.end(), '.'), str.end());
+        return str;
+    }
+
+    const std::string A_EXT_B_INT = "***.***.1**";
+    const std::string A_INT_B_INT = "1**.***.***";
+
+
+};
+
+typedef test_group<test_relatepredicate_data> group;
+typedef group::object object;
+
+group test_relatepredicate_group("geos::operation::relateng::RelatePredicate");
+
+//
+// Test Cases
+//
+
+// testIntersects
+template<>
+template<>
+void object::test<1> ()
+{
+    checkPredicate(*RelatePredicate::intersects(), A_INT_B_INT, true);
+}
+
+// testDisjoint
+template<>
+template<>
+void object::test<2> ()
+{
+    checkPredicate(*RelatePredicate::intersects(), A_EXT_B_INT, false);
+    checkPredicate(*RelatePredicate::disjoint(), A_EXT_B_INT, true);
+}
+
+// testCovers
+template<>
+template<>
+void object::test<3> ()
+{
+    checkPredicate(*RelatePredicate::covers(), A_INT_B_INT, true);
+    checkPredicate(*RelatePredicate::covers(), A_EXT_B_INT, false);
+}
+
+// testCoversFast
+template<>
+template<>
+void object::test<4> ()
+{
+    checkPredicatePartial(*RelatePredicate::covers(), A_EXT_B_INT, false);
+}
+
+// testMatch
+template<>
+template<>
+void object::test<5> ()
+{
+    checkPredicate(*RelatePredicate::matches("1***T*0**"), "1**.*2*.0**", true);
+}
+
+
+} // namespace tut
diff --git a/tests/unit/tut/tut.hpp b/tests/unit/tut/tut.hpp
index 01bb978fc..f8d464819 100644
--- a/tests/unit/tut/tut.hpp
+++ b/tests/unit/tut/tut.hpp
@@ -149,7 +149,7 @@ struct tests_registerer<Test, Group, 0>
  * each new test since we have to have reinitialized
  * Data base class.
  */
-template <class Data, int MaxTestsInGroup = 50>
+template <class Data, int MaxTestsInGroup = 100>
 class test_group : public group_base, public test_group_posix
 {
     test_group(const test_group&);
diff --git a/tests/xmltester/tests/misc/InvalidRelates.xml b/tests/xmltester/tests/misc/InvalidRelates.xml
index d1c2b7125..69b80bdd6 100644
--- a/tests/xmltester/tests/misc/InvalidRelates.xml
+++ b/tests/xmltester/tests/misc/InvalidRelates.xml
@@ -38,7 +38,7 @@
       ((20 180, 20 80, 180 80, 180 180, 20 180)))
     </b>
     <test>
-      <op arg1="A" arg2="B" arg3="1FF0FF212" name="relate">true</op>
+      <op arg1="A" arg2="B" arg3="1FFF0F212" name="relate">true</op>
     </test>
   </case>
 
@@ -53,7 +53,7 @@
       ((60 180, 60 80, 180 80, 180 180, 60 180)))
     </b>
     <test>
-      <op arg1="A" arg2="B" arg3="11F00F212" name="relate">true</op>
+      <op arg1="A" arg2="B" arg3="111F0F212" name="relate">true</op>
     </test>
   </case>
 
diff --git a/tests/xmltester/tests/validate/TestRelatePL.xml b/tests/xmltester/tests/validate/TestRelatePL.xml
index 2e11fb7b9..a0d1b9066 100644
--- a/tests/xmltester/tests/validate/TestRelatePL.xml
+++ b/tests/xmltester/tests/validate/TestRelatePL.xml
@@ -40,7 +40,7 @@
   <test><op name="covers"     arg1="A" arg2="B">true</op></test>
   <test><op name="crosses"    arg1="A" arg2="B">false</op></test>
   <test><op name="disjoint"   arg1="A" arg2="B">false</op></test>
-  <test><op name="equalsTopo" arg1="A" arg2="B">false</op></test>
+  <test><op name="equalsTopo" arg1="A" arg2="B">true</op></test>
   <test><op name="intersects" arg1="A" arg2="B">true</op></test>
   <test><op name="overlaps"   arg1="A" arg2="B">false</op></test>
   <test><op name="touches"    arg1="A" arg2="B">false</op></test>

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

Summary of changes:
 .github/workflows/ci.yml                           |   2 +-
 NEWS.md                                            |   9 +
 benchmarks/BenchmarkUtils.h                        |   6 +
 benchmarks/geom/CMakeLists.txt                     |   8 +-
 .../geom/PreparedPolygonIntersectsPerfTest.cpp     |  89 ---
 benchmarks/geom/TopologyPredicatePerfTest.cpp      | 266 +++++++
 capi/geos_c.cpp                                    |  12 +
 capi/geos_c.h.in                                   |  51 ++
 capi/geos_ts_c.cpp                                 |  66 +-
 include/geos/algorithm/PolygonNodeTopology.h       |  32 +
 include/geos/constants.h                           |   1 +
 include/geos/geom/Coordinate.h                     |   5 +
 include/geos/geom/Geometry.h                       |   6 +-
 include/geos/geom/GeometryCollection.h             |   1 +
 include/geos/geom/IntersectionMatrix.h             |   3 +-
 include/geos/geom/prep/BasicPreparedGeometry.h     |  27 +-
 include/geos/geom/prep/PreparedGeometry.h          |  24 +
 .../util/{GeometryExtracter.h => GeometryLister.h} |  45 +-
 include/geos/index/chain/MonotoneChain.h           |   7 +
 include/geos/io/WKTWriter.h                        |   2 +-
 .../noding/MCIndexSegmentSetMutualIntersector.h    |   2 +-
 include/geos/noding/SegmentString.h                |  36 +
 .../geos/operation/relateng/AdjacentEdgeLocator.h  | 124 +++
 include/geos/operation/relateng/BasicPredicate.h   | 105 +++
 .../geos/operation/relateng/DimensionLocation.h    |  60 ++
 .../operation/relateng/EdgeSegmentIntersector.h    |  80 ++
 .../operation/relateng/EdgeSegmentOverlapAction.h  |  75 ++
 .../geos/operation/relateng/EdgeSetIntersector.h   |  95 +++
 include/geos/operation/relateng/IMPatternMatcher.h |  87 ++
 include/geos/operation/relateng/IMPredicate.h      | 131 +++
 .../operation/relateng/IntersectionMatrixPattern.h |  65 ++
 .../geos/operation/relateng/LineStringExtracter.h  |  77 ++
 include/geos/operation/relateng/LinearBoundary.h   |  88 +++
 include/geos/operation/relateng/NodeSection.h      | 166 ++++
 include/geos/operation/relateng/NodeSections.h     | 102 +++
 .../geos/operation/relateng/PolygonNodeConverter.h | 112 +++
 include/geos/operation/relateng/RelateEdge.h       | 176 +++++
 include/geos/operation/relateng/RelateGeometry.h   | 234 ++++++
 .../operation/relateng/RelateMatrixPredicate.h     |  85 ++
 include/geos/operation/relateng/RelateNG.h         | 282 +++++++
 include/geos/operation/relateng/RelateNode.h       | 146 ++++
 .../geos/operation/relateng/RelatePointLocator.h   | 213 +++++
 include/geos/operation/relateng/RelatePredicate.h  | 652 +++++++++++++++
 .../geos/operation/relateng/RelateSegmentString.h  | 155 ++++
 include/geos/operation/relateng/TopologyComputer.h | 226 ++++++
 .../geos/operation/relateng/TopologyPredicate.h    | 206 +++++
 include/geos/operation/valid/IsSimpleOp.h          |   6 +-
 src/algorithm/PolygonNodeTopology.cpp              |  61 +-
 src/geom/Geometry.cpp                              |  64 ++
 src/geom/GeometryCollection.cpp                    |   2 +
 src/geom/prep/BasicPreparedGeometry.cpp            |  44 +-
 src/io/WKTWriter.cpp                               |   8 +-
 src/operation/relateng/AdjacentEdgeLocator.cpp     | 159 ++++
 src/operation/relateng/BasicPredicate.cpp          | 137 ++++
 src/operation/relateng/DimensionLocation.cpp       | 125 +++
 src/operation/relateng/EdgeSegmentIntersector.cpp  | 114 +++
 .../relateng/EdgeSegmentOverlapAction.cpp}         |  25 +-
 src/operation/relateng/EdgeSetIntersector.cpp      |  99 +++
 src/operation/relateng/IMPatternMatcher.cpp        | 149 ++++
 src/operation/relateng/IMPredicate.cpp             | 155 ++++
 src/operation/relateng/LineStringExtracter.cpp     |  87 ++
 src/operation/relateng/LinearBoundary.cpp          | 114 +++
 src/operation/relateng/NodeSection.cpp             | 253 ++++++
 src/operation/relateng/NodeSections.cpp            | 163 ++++
 src/operation/relateng/PolygonNodeConverter.cpp    | 193 +++++
 src/operation/relateng/RelateEdge.cpp              | 475 +++++++++++
 src/operation/relateng/RelateGeometry.cpp          | 554 +++++++++++++
 src/operation/relateng/RelateNG.cpp                | 699 ++++++++++++++++
 src/operation/relateng/RelateNode.cpp              | 323 ++++++++
 src/operation/relateng/RelatePointLocator.cpp      | 331 ++++++++
 src/operation/relateng/RelatePredicate.cpp         | 109 +++
 src/operation/relateng/RelateSegmentString.cpp     | 185 +++++
 src/operation/relateng/TopologyComputer.cpp        | 572 ++++++++++++++
 tests/unit/capi/GEOSMinimumClearanceTest.cpp       |   2 +-
 tests/unit/capi/GEOSPreparedGeometryTest.cpp       |  36 +-
 tests/unit/capi/GEOSRelateBoundaryNodeRuleTest.cpp |   2 +-
 tests/unit/capi/GEOSRelatePatternTest.cpp          |  10 +-
 tests/unit/capi/capi_test_utils.h                  |  34 +-
 tests/unit/geom/prep/PreparedGeometryTest.cpp      |   1 +
 tests/unit/geom/util/GeometryListerTest.cpp        |  62 ++
 tests/unit/io/WKBWriterTest.cpp                    |  10 +-
 .../operation/relateng/AdjacentEdgeLocatorTest.cpp | 129 +++
 .../unit/operation/relateng/LinearBoundaryTest.cpp | 147 ++++
 .../relateng/PolygonNodeConverterTest.cpp          | 212 +++++
 .../unit/operation/relateng/RelateGeometryTest.cpp | 138 ++++
 tests/unit/operation/relateng/RelateNGGCTest.cpp   | 315 ++++++++
 .../operation/relateng/RelateNGRobustnessTest.cpp  | 282 +++++++
 tests/unit/operation/relateng/RelateNGTest.cpp     | 879 +++++++++++++++++++++
 tests/unit/operation/relateng/RelateNGTest.h       | 134 ++++
 .../operation/relateng/RelatePointLocatorTest.cpp  | 149 ++++
 .../operation/relateng/RelatePredicateTest.cpp     | 138 ++++
 tests/unit/tut/tut.hpp                             |   2 +-
 tests/xmltester/tests/misc/InvalidRelates.xml      |   4 +-
 tests/xmltester/tests/validate/TestRelatePL.xml    |   2 +-
 94 files changed, 12105 insertions(+), 231 deletions(-)
 delete mode 100644 benchmarks/geom/PreparedPolygonIntersectsPerfTest.cpp
 create mode 100644 benchmarks/geom/TopologyPredicatePerfTest.cpp
 copy include/geos/geom/util/{GeometryExtracter.h => GeometryLister.h} (55%)
 create mode 100644 include/geos/operation/relateng/AdjacentEdgeLocator.h
 create mode 100644 include/geos/operation/relateng/BasicPredicate.h
 create mode 100644 include/geos/operation/relateng/DimensionLocation.h
 create mode 100644 include/geos/operation/relateng/EdgeSegmentIntersector.h
 create mode 100644 include/geos/operation/relateng/EdgeSegmentOverlapAction.h
 create mode 100644 include/geos/operation/relateng/EdgeSetIntersector.h
 create mode 100644 include/geos/operation/relateng/IMPatternMatcher.h
 create mode 100644 include/geos/operation/relateng/IMPredicate.h
 create mode 100644 include/geos/operation/relateng/IntersectionMatrixPattern.h
 create mode 100644 include/geos/operation/relateng/LineStringExtracter.h
 create mode 100644 include/geos/operation/relateng/LinearBoundary.h
 create mode 100644 include/geos/operation/relateng/NodeSection.h
 create mode 100644 include/geos/operation/relateng/NodeSections.h
 create mode 100644 include/geos/operation/relateng/PolygonNodeConverter.h
 create mode 100644 include/geos/operation/relateng/RelateEdge.h
 create mode 100644 include/geos/operation/relateng/RelateGeometry.h
 create mode 100644 include/geos/operation/relateng/RelateMatrixPredicate.h
 create mode 100644 include/geos/operation/relateng/RelateNG.h
 create mode 100644 include/geos/operation/relateng/RelateNode.h
 create mode 100644 include/geos/operation/relateng/RelatePointLocator.h
 create mode 100644 include/geos/operation/relateng/RelatePredicate.h
 create mode 100644 include/geos/operation/relateng/RelateSegmentString.h
 create mode 100644 include/geos/operation/relateng/TopologyComputer.h
 create mode 100644 include/geos/operation/relateng/TopologyPredicate.h
 create mode 100644 src/operation/relateng/AdjacentEdgeLocator.cpp
 create mode 100644 src/operation/relateng/BasicPredicate.cpp
 create mode 100644 src/operation/relateng/DimensionLocation.cpp
 create mode 100644 src/operation/relateng/EdgeSegmentIntersector.cpp
 copy src/{index/chain/MonotoneChainOverlapAction.cpp => operation/relateng/EdgeSegmentOverlapAction.cpp} (57%)
 create mode 100644 src/operation/relateng/EdgeSetIntersector.cpp
 create mode 100644 src/operation/relateng/IMPatternMatcher.cpp
 create mode 100644 src/operation/relateng/IMPredicate.cpp
 create mode 100644 src/operation/relateng/LineStringExtracter.cpp
 create mode 100644 src/operation/relateng/LinearBoundary.cpp
 create mode 100644 src/operation/relateng/NodeSection.cpp
 create mode 100644 src/operation/relateng/NodeSections.cpp
 create mode 100644 src/operation/relateng/PolygonNodeConverter.cpp
 create mode 100644 src/operation/relateng/RelateEdge.cpp
 create mode 100644 src/operation/relateng/RelateGeometry.cpp
 create mode 100644 src/operation/relateng/RelateNG.cpp
 create mode 100644 src/operation/relateng/RelateNode.cpp
 create mode 100644 src/operation/relateng/RelatePointLocator.cpp
 create mode 100644 src/operation/relateng/RelatePredicate.cpp
 create mode 100644 src/operation/relateng/RelateSegmentString.cpp
 create mode 100644 src/operation/relateng/TopologyComputer.cpp
 create mode 100644 tests/unit/geom/util/GeometryListerTest.cpp
 create mode 100644 tests/unit/operation/relateng/AdjacentEdgeLocatorTest.cpp
 create mode 100644 tests/unit/operation/relateng/LinearBoundaryTest.cpp
 create mode 100644 tests/unit/operation/relateng/PolygonNodeConverterTest.cpp
 create mode 100644 tests/unit/operation/relateng/RelateGeometryTest.cpp
 create mode 100644 tests/unit/operation/relateng/RelateNGGCTest.cpp
 create mode 100644 tests/unit/operation/relateng/RelateNGRobustnessTest.cpp
 create mode 100644 tests/unit/operation/relateng/RelateNGTest.cpp
 create mode 100644 tests/unit/operation/relateng/RelateNGTest.h
 create mode 100644 tests/unit/operation/relateng/RelatePointLocatorTest.cpp
 create mode 100644 tests/unit/operation/relateng/RelatePredicateTest.cpp


hooks/post-receive
-- 
GEOS


More information about the geos-commits mailing list