Repository: GEOSwift/GEOSwift (swift)

Issue:
Searching for point within a large geoJSON file
Is there an efficient built in method for finding which features within a geojson file contains a point without resorting to inefficient linear search? I've got a very large file that I'm having to linear search every location update and it's inducing heaps of lag.

```
func findSpeedLimit(location: CLLocationCoordinate2D, geoJSON: GeoJSON) -> Int? {
    // Decode the GeoJSON data into a FeatureCollection object
    guard case let .featureCollection(featureCollection) = geoJSON else {
        print("Error: GeoJSON data is not a FeatureCollection")
        return nil
    }
    
    // Create a Point object from the location coordinates
    let point = Point(x: location.longitude, y: location.latitude)
    //print("Looking for speed limit at location: \(location.latitude), \(location.longitude)")
    
    // Loop through the features in the feature collection
    for (index, feature) in featureCollection.features.enumerated() {
        // Check if the feature has a geometry and properties
        guard let geometry = feature.geometry,
              let properties = feature.properties else {
            print("Feature \(index) has no geometry or properties")
            continue
        }
        
        do {
            // Check if the geometry contains the point
            if try geometry.contains(point) {
                // Check if the properties have a speedLimitZoneValue key
                if let speedLimit = properties["speedLimitZoneValue"] {
                    let speedLimitString = String(describing: speedLimit)
                    if let number = Int(speedLimitString.components(separatedBy: CharacterSet.decimalDigits.inverted).joined()) {
                        //print (number)
                        //print("Speed limit found for feature \(index): \(number) km/h")
                        return number
                    }
                    
                } else {
                    print("Feature \(index) has no 'speedLimitZoneValue' property")
                }
            } else {
                //print("Feature \(index) does not contain the point")
            }
        } catch {
            print("Error checking if geometry contains point: \(error)")
        }
    }
    
    return nil
}
```

Pull request description:
Fixes #263

Patch:
diff --git a/Sources/GEOSwift/GEOS/GeometryConvertible+GEOS.swift b/Sources/GEOSwift/GEOS/GeometryConvertible+GEOS.swift
index cfd230ec..6e968711 100644
--- a/Sources/GEOSwift/GEOS/GeometryConvertible+GEOS.swift
+++ b/Sources/GEOSwift/GEOS/GeometryConvertible+GEOS.swift
@@ -209,6 +209,14 @@ public extension GeometryConvertible {
         try evaluateBinaryPredicate(GEOSCoveredBy_r, with: geometry)
     }
 
+    // MARK: - Prepared Geometry
+
+    func makePrepared() throws -> PreparedGeometry {
+        let context = try GEOSContext()
+        let geosObject = try geometry.geosObject(with: context)
+        return try PreparedGeometry(context: context, base: geosObject)
+    }
+
     // MARK: - Dimensionally Extended 9 Intersection Model Functions
 
     /// Parameter mask: A DE9-IM mask pattern
diff --git a/Sources/GEOSwift/GEOS/PreparedGeometry.swift b/Sources/GEOSwift/GEOS/PreparedGeometry.swift
new file mode 100644
index 00000000..64666369
--- /dev/null
+++ b/Sources/GEOSwift/GEOS/PreparedGeometry.swift
@@ -0,0 +1,41 @@
+import geos
+
+/// A prepared geometry. Unlike most of GEOSwift, this type is not thread safe.
+public final class PreparedGeometry {
+    let context: GEOSContext
+    private let base: GEOSObject
+    let pointer: OpaquePointer
+
+    init(context: GEOSContext, base: GEOSObject) throws {
+        self.context = context
+        self.base = base
+        guard let pointer = GEOSPrepare_r(context.handle, base.pointer) else {
+            throw GEOSError.libraryError(errorMessages: context.errors)
+        }
+        self.pointer = pointer
+    }
+
+    deinit {
+        GEOSPreparedGeom_destroy_r(context.handle, pointer)
+    }
+
+    public func contains(_ geometry: GeometryConvertible) throws -> Bool {
+        let geosObject = try geometry.geometry.geosObject(with: context)
+        // returns 1 on true, 0 on false, 2 on exception
+        let result = GEOSPreparedContains_r(context.handle, pointer, geosObject.pointer)
+        guard result != 2 else {
+            throw GEOSError.libraryError(errorMessages: context.errors)
+        }
+        return result == 1
+    }
+
+    public func containsProperly(_ geometry: GeometryConvertible) throws -> Bool {
+        let geosObject = try geometry.geometry.geosObject(with: context)
+        // returns 1 on true, 0 on false, 2 on exception
+        let result = GEOSPreparedContainsProperly_r(context.handle, pointer, geosObject.pointer)
+        guard result != 2 else {
+            throw GEOSError.libraryError(errorMessages: context.errors)
+        }
+        return result == 1
+    }
+}


Test patch:
diff --git a/Tests/GEOSwiftTests/GEOS/GeometryConvertible+GEOSTests.swift b/Tests/GEOSwiftTests/GEOS/GeometryConvertible+GEOSTests.swift
index 84185d3e..8516e80b 100644
--- a/Tests/GEOSwiftTests/GEOS/GeometryConvertible+GEOSTests.swift
+++ b/Tests/GEOSwiftTests/GEOS/GeometryConvertible+GEOSTests.swift
@@ -496,6 +496,18 @@ final class GeometryConvertible_GEOSTests: XCTestCase {
         }
     }
 
+    // MARK: - Prepared Geometry
+
+    func testMakePreparedAllTypes() {
+        for g in geometryConvertibles {
+            do {
+                _ = try g.makePrepared()
+            } catch {
+                XCTFail("Unexpected error for \(g) makePrepared() \(error)")
+            }
+        }
+    }
+
     // MARK: - Dimensionally Extended 9 Intersection Model Functions
 
     func testRelateMaskBetweenPoints() {
diff --git a/Tests/GEOSwiftTests/GEOS/PreparedGeometryTests.swift b/Tests/GEOSwiftTests/GEOS/PreparedGeometryTests.swift
new file mode 100644
index 00000000..1b7d6b2b
--- /dev/null
+++ b/Tests/GEOSwiftTests/GEOS/PreparedGeometryTests.swift
@@ -0,0 +1,50 @@
+//
+//  File.swift
+//  
+//
+//  Created by Andrew Hershberger on 4/22/23.
+//
+
+import XCTest
+import GEOSwift
+
+final class PreparedGeometryTests: XCTestCase {
+
+    let geometryConvertibles: [GeometryConvertible] = [
+        Point.testValue1,
+        Geometry.point(.testValue1),
+        MultiPoint.testValue,
+        Geometry.multiPoint(.testValue),
+        LineString.testValue1,
+        Geometry.lineString(.testValue1),
+        MultiLineString.testValue,
+        Geometry.multiLineString(.testValue),
+        Polygon.LinearRing.testValueHole1,
+        Polygon.testValueWithHole,
+        Geometry.polygon(.testValueWithHole),
+        MultiPolygon.testValue,
+        Geometry.multiPolygon(.testValue),
+        GeometryCollection.testValue,
+        GeometryCollection.testValueWithRecursion,
+        Geometry.geometryCollection(.testValue)]
+
+    func testContainsAllTypes() {
+        for (g1, g2) in geometryConvertibles.allPairs {
+            do {
+                _ = try g1.makePrepared().contains(g2)
+            } catch {
+                XCTFail("Unexpected error for \(g1) makePrepared().contains( \(g2)) \(error)")
+            }
+        }
+    }
+
+    func testContainsProperlyAllTypes() {
+        for (g1, g2) in geometryConvertibles.allPairs {
+            do {
+                _ = try g1.makePrepared().containsProperly(g2)
+            } catch {
+                XCTFail("Unexpected error for \(g1) makePrepared().containsProperly( \(g2)) \(error)")
+            }
+        }
+    }
+}