tg_geometry 0.3.2 → 0.3.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 7cb54eb85cd48036c2f32eb5f95fa3d19cbf994d375bc102b05f2d45dfb1f8ec
4
- data.tar.gz: c5ff1c066708d8826c125185f34a346e1f1e2bf66f65fb1b1eb13e75bf955aad
3
+ metadata.gz: 27136bafb4a0114a2921af0413c047391bfffb38a6ff41f6c4f011f7cf4ab2ad
4
+ data.tar.gz: 7b2148e42f78fa74b3d5ac41e492e78673dd2617aa887c0023affcfa8fe555bf
5
5
  SHA512:
6
- metadata.gz: 981e37eb58fac8eaa20c3fa9604b751c263330e3f3dc2e8614cb1c91e36eb76d31431b4a13b3cbcd44475323d5ca76ec3e58c013d8c16de9d28e2ec890704962
7
- data.tar.gz: 05db4ca563cd1c22e13d5c27e440845452397f50516cf01dde8675032fb3e12a527ba0fe52ccd32bc1fe313b3f8d083f46c17a54e4e8c316b6e8ce9766c278d5
6
+ metadata.gz: e91a5dd12b3240540e9f6171c9c8a3c0f959d9e24779e392ae6c02c2e478712d2585bff5e37b3524bdf7d3b8581ebb30e14029b4aa038eb549229f92f0eee388
7
+ data.tar.gz: 806918f8eaaa7025b0693ef07e6d244e37cdb4998ff4cc5845a752c4b200f2cb341391a43f37afbdb696525e18d3415f55833c8d09712c31161eb2e879e179f5
data/CHANGELOG.md CHANGED
@@ -1,5 +1,17 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.3.3 - 28.10.2026
4
+
5
+ ### Changed
6
+
7
+ - Updated vendored `tidwall/tg`, `rtree.c` to v0.6.1, and `json.c` to pinned upstream revisions.
8
+ - Vendor sync now removes trailing whitespace from C sources and headers.
9
+
10
+ ### Fixed
11
+
12
+ - Avoided querying TG parse errors when geometry allocation fails in FeatureSource.
13
+ - Corrected JSON UTF-16 surrogate boundary handling through the updated `json.c` vendor.
14
+
3
15
  ## 0.3.2 - unreleased
4
16
 
5
17
  ### Added
data/README.md CHANGED
@@ -8,7 +8,25 @@ It exposes the public Ruby namespace `TG::Geometry`:
8
8
  require "tg/geometry"
9
9
  ```
10
10
 
11
- The gem is focused on fast in-process planar geometry parsing, predicates, format conversion, GeoJSON FeatureCollection imports, and immutable geofencing indexes. It is not a full GIS system.
11
+ The gem is focused on fast in-process planar geometry parsing, predicates, format conversion, GeoJSON FeatureCollection imports, point-to-geometry distance queries, and immutable geofencing indexes. It is not a full GIS system.
12
+
13
+ ## Start here
14
+
15
+ For runnable examples, use **[GET_STARTED.md](GET_STARTED.md)**.
16
+
17
+ That guide shows the normal path from install to real queries:
18
+
19
+ - parsing GeoJSON/WKT/WKB/Hex/GeoBIN;
20
+ - direct Ruby constructors;
21
+ - immutable `Index` builds;
22
+ - point geofencing queries;
23
+ - packed batch point queries;
24
+ - point-to-geometry distance in planar XY and approximate lon/lat meters;
25
+ - GeoJSON FeatureCollection imports;
26
+ - SRID/EWKB conversion;
27
+ - nearest segment helpers;
28
+ - Registry and optional ActiveRecord integration;
29
+ - safe immutable-index reload pattern.
12
30
 
13
31
  ## Installation
14
32
 
@@ -26,317 +44,86 @@ The extension builds from vendored C sources. It does not require GEOS, PostGIS,
26
44
 
27
45
  Supported platforms: Linux and macOS on x86_64/aarch64. Windows is not supported for the first release.
28
46
 
29
- ## Parsing and predicates
30
-
31
- ```ruby
32
- zone = TG::Geometry.parse_geojson(<<~JSON)
33
- {
34
- "type": "Polygon",
35
- "coordinates": [[[0,0], [10,0], [10,10], [0,10], [0,0]]]
36
- }
37
- JSON
38
-
39
- zone.frozen? # => true
40
- zone.type # => :polygon
41
- zone.covers_xy?(5, 5) # => true
42
- zone.covers_xy?(0, 0) # => true, boundary is covered
43
- zone.bbox # => #<TG::Geometry::Rect ...>
44
- zone.to_wkt
45
- zone.to_wkb
46
- ```
47
-
48
- Parse shortcuts:
49
-
50
- ```ruby
51
- TG::Geometry.parse(str, format: :auto, index: :ystripes)
52
- TG::Geometry.parse_geojson(str, index: :ystripes)
53
- TG::Geometry.parse_wkt(str, index: :ystripes)
54
- TG::Geometry.parse_wkb(bytes, index: :ystripes)
55
- TG::Geometry.parse_hex(str, index: :ystripes)
56
- TG::Geometry.parse_geobin(bytes, index: :ystripes)
57
- ```
58
-
59
- `TG::Geometry::Geom` objects are immutable and cannot be manually allocated or manually freed from Ruby.
60
-
61
- ## Constructors
62
-
63
- Construct simple planar geometries directly from Ruby arrays without parsing strings or bytes:
64
-
65
- ```ruby
66
- line = TG::Geometry.line_string([[0.0, 0.0], [10.0, 0.0]], index: :natural, srid: 4326)
67
- poly = TG::Geometry.polygon(
68
- [[0, 0], [10, 0], [10, 10], [0, 10], [0, 0]],
69
- holes: [[[2, 2], [4, 2], [4, 4], [2, 4], [2, 2]]],
70
- index: :ystripes,
71
- srid: 4326
72
- )
73
- mp = TG::Geometry.multi_polygon([
74
- { exterior: [[0, 0], [1, 0], [1, 1], [0, 0]], holes: [] },
75
- [[10, 10], [11, 10], [11, 11], [10, 10]]
76
- ])
77
- ```
78
-
79
- Constructors are strict: rings must already be closed, invalid coordinates raise `TG::Geometry::ArgumentError`, and no winding/self-intersection fixes are performed. `srid:` is metadata on the wrapper; it does not alter coordinates or perform reprojection.
80
-
81
- ## SRID and EWKB
82
-
83
- `parse_wkb` and `parse_hex` preserve EWKB SRID metadata when the EWKB SRID flag is present:
84
-
85
- ```ruby
86
- geom = TG::Geometry.parse_wkb(postgis_bytea)
87
- geom.srid # => 4326, 3857, 0, or nil for plain WKB
88
- ```
47
+ ## API map
89
48
 
90
- `to_wkb` always writes plain WKB. Use `to_ewkb` when a PostGIS-compatible SRID-bearing payload is required:
49
+ This README intentionally stays compact. The runnable examples live in [GET_STARTED.md](GET_STARTED.md); deeper design notes live in `docs/`.
91
50
 
92
- ```ruby
93
- ewkb = geom.to_ewkb # uses geom.srid
94
- ewkb = geom.to_ewkb(srid: 4326) # explicit override
95
- ```
51
+ ### Parsing and format conversion
96
52
 
97
- SRID is metadata only. `tg_geometry` does not check SRID compatibility, transform coordinates, or calculate geodesic distances.
53
+ `TG::Geometry.parse` auto-detects GeoJSON, WKT, WKB, Hex, and GeoBIN. Explicit shortcuts are also available: `parse_geojson`, `parse_wkt`, `parse_wkb`, `parse_hex`, and `parse_geobin`.
98
54
 
99
- ## Rect
55
+ `TG::Geometry::Geom` objects are immutable and can be converted back to WKT/WKB/GeoJSON where supported.
100
56
 
101
- ```ruby
102
- rect = TG::Geometry::Rect.new(0, 0, 10, 10)
103
- rect.center # => [5.0, 5.0]
104
- rect.contains_point?(5, 5) # => true
105
- rect.intersects?(other_rect)
106
- rect.expand_to_include(other_rect)
107
- rect.expand_to_include_point(x, y)
108
- ```
57
+ See [GET_STARTED.md#2-parse-a-geometry](GET_STARTED.md#2-parse-a-geometry).
109
58
 
110
- `Rect` rejects non-finite coordinates and invalid coordinate order. It is frozen after construction.
59
+ ### Constructors
111
60
 
112
- ## Immutable Index
61
+ Planar geometries can be constructed directly from Ruby arrays without parsing strings or bytes: point, multipoint, line string, polygon, multiline, multipolygon, and geometry collection helpers are available.
113
62
 
114
- ```ruby
115
- entries = [
116
- [:zone_a, '{"type":"Polygon","coordinates":[[[0,0],[10,0],[10,10],[0,10],[0,0]]]}'],
117
- [:zone_b, '{"type":"Polygon","coordinates":[[[20,20],[30,20],[30,30],[20,30],[20,20]]]}']
118
- ]
119
-
120
- index = TG::Geometry::Index.build(
121
- entries,
122
- via: :geojson,
123
- strategy: :rtree,
124
- predicate: :covers,
125
- geometry_index: :ystripes
126
- )
127
-
128
- index.frozen? # => true
129
- index.size # => 2
130
- index.strategy # => :rtree
131
- index.predicate # => :covers
132
- index.find_covering(5, 5) # => :zone_a
133
- index.covering_ids(5, 5) # => [:zone_a]
134
- index.intersecting_rect(0, 0, 25, 25)
135
- ```
63
+ See [GET_STARTED.md#3-construct-geometries-directly](GET_STARTED.md#3-construct-geometries-directly).
136
64
 
137
- Accepted input shape:
65
+ ### SRID and EWKB
138
66
 
139
- ```ruby
140
- [[id1, object1], [id2, object2], ...]
141
- ```
67
+ EWKB SRID metadata is preserved on parse when present. `to_wkb` writes plain WKB; `to_ewkb` writes a PostGIS-compatible SRID-bearing payload. SRID is metadata only: no coordinate transformation is performed.
142
68
 
143
- Rules:
69
+ See [GET_STARTED.md#8-preserve-srid-and-write-ewkb](GET_STARTED.md#8-preserve-srid-and-write-ewkb) and [docs/SRID_AND_EWKB.md](docs/SRID_AND_EWKB.md).
144
70
 
145
- - `entries` must be an Array.
146
- - Every entry must be a two-element Array.
147
- - `id` may be any Ruby object except `nil`.
148
- - Duplicate ids are allowed.
149
- - Returned ids are the same Ruby objects stored in the index.
150
- - Result order is insertion order for both `:flat` and `:rtree`.
71
+ ### Rect
151
72
 
152
- Accepted `via:` modes:
73
+ `TG::Geometry::Rect` is an immutable helper for bounding boxes, containment checks, intersections, and expansion.
153
74
 
154
- - `:geom` — borrow an existing `TG::Geometry::Geom` and keep its owner alive.
155
- - `:geojson` — parse and own native TG geometries inside the index.
156
- - `:wkb` — parse and own native TG geometries inside the index.
75
+ See [GET_STARTED.md#4-use-rect-helpers](GET_STARTED.md#4-use-rect-helpers).
157
76
 
158
- Accepted strategies:
77
+ ### Immutable Index
159
78
 
160
- - `:flat`
161
- - `:rtree`
79
+ `TG::Geometry::Index` stores immutable entries with explicit `via:`, `strategy:`, and `predicate:` options. `:rtree` builds a spatial index; `:flat` does a linear scan with bbox prefiltering. There is no `:auto` strategy by design.
162
80
 
163
- Accepted predicates:
81
+ See [GET_STARTED.md#5-build-an-immutable-index](GET_STARTED.md#5-build-an-immutable-index).
164
82
 
165
- - `:covers` — default for geofencing; boundary points are included.
166
- - `:contains` — stricter containment semantics.
83
+ ### Point and geometry queries
167
84
 
168
- The `predicate:` option affects only the legacy point-based query methods (`find_covering`, `covering_ids(x, y)`, `covering_ids_batch_packed`). The geometry-based query methods (`intersecting_geom_ids`, `covering_geom_ids`, `containing_geom_ids`) use their own predicates based on the method name.
85
+ Index methods cover point geofencing, packed batch point queries, rectangle intersection, and geometry-vs-geometry predicates.
169
86
 
170
- `strategy: :auto` is intentionally not exposed. Choose the strategy explicitly and benchmark on your own data.
87
+ See:
171
88
 
172
- ## Geometry-based index queries
89
+ - [GET_STARTED.md#5-build-an-immutable-index](GET_STARTED.md#5-build-an-immutable-index)
90
+ - [GET_STARTED.md#6-batch-point-queries](GET_STARTED.md#6-batch-point-queries)
91
+ - [docs/GEOMETRY_QUERIES.md](docs/GEOMETRY_QUERIES.md)
173
92
 
174
- ```ruby
175
- query = TG::Geometry.polygon([[1, 1], [2, 1], [2, 2], [1, 1]])
176
- index.intersecting_geom_ids(query)
177
- index.covering_geom_ids(query)
178
- index.containing_geom_ids(query)
179
- ```
180
-
181
- Predicate direction is explicit:
182
-
183
- | Method | Predicate direction | Boundary semantics |
184
- | --- | --- | --- |
185
- | `intersecting_geom_ids(query)` | stored geom intersects query | any intersection |
186
- | `covering_geom_ids(query)` | stored geom covers query | boundary included |
187
- | `containing_geom_ids(query)` | stored geom contains query | strict interior; boundary excluded |
93
+ ### Point-to-geometry distance
188
94
 
189
- Results are ids only and preserve insertion order. Duplicate ids remain possible if duplicate ids were inserted.
95
+ Distance APIs are explicit about units:
190
96
 
97
+ - `*_xy` returns input coordinate units;
98
+ - `*_lnglat_meters` returns approximate local meters for lon/lat geofencing.
191
99
 
100
+ There is no `metric:` option, no automatic lon/lat-vs-XY detection, no kNN index, and no geodesic/great-circle segment distance.
192
101
 
193
- ## Point-to-geometry distance
102
+ See [GET_STARTED.md#7-measure-distance-to-a-zone](GET_STARTED.md#7-measure-distance-to-a-zone) and [docs/GEOMETRY_QUERIES.md](docs/GEOMETRY_QUERIES.md).
194
103
 
195
- `TG::Geometry::Geom` exposes explicit point distance APIs. Units are encoded in the method names; there is no `metric:` option and no automatic lng/lat-vs-XY detection.
196
-
197
- ```ruby
198
- zone.distance_to_lnglat_meters(lng, lat) # => Float, approximate meters
199
- zone.boundary_distance_to_lnglat_meters(lng, lat) # => Float, approximate meters
200
- zone.nearest_point_lnglat(lng, lat) # => [lng, lat]
104
+ ### GeoJSON FeatureSource
201
105
 
202
- zone.distance_to_xy(x, y) # => Float in input coordinate units
203
- zone.boundary_distance_to_xy(x, y) # => Float in input coordinate units
204
- zone.nearest_point_xy(x, y) # => [x, y]
205
- ```
106
+ `TG::Geometry::FeatureSource` reads GeoJSON `FeatureCollection` sources without `JSON.parse` of the whole document into Ruby Hash/Array objects. It can return index entries, feature triples with raw properties JSON, reports, or build an index directly.
206
107
 
207
- For areal geometries (`Polygon`, `MultiPolygon`, and areal collection members), `distance_to_*` returns `0.0` for points inside the covered area or on the boundary. Holes are excluded, so a point inside a hole measures to the nearest hole ring. `boundary_distance_to_*` always measures to the nearest boundary/ring/segment; for an interior point it does not return `0.0` merely because the point is covered. `nearest_point_*` returns the nearest boundary point for areal geometries, including interior queries.
108
+ See [GET_STARTED.md#9-import-a-geojson-featurecollection](GET_STARTED.md#9-import-a-geojson-featurecollection) and [docs/FEATURE_SOURCE.md](docs/FEATURE_SOURCE.md).
208
109
 
209
- Distance methods for lng/lat geometries return approximate meters using a per-query local equirectangular frame. Segments are GeoJSON straight coordinate segments, not great-circle arcs. This is geofencing-grade metric distance, not geodesy. Accuracy is intended for local geofencing and degrades with latitude separation.
110
+ ### Nearest segment
210
111
 
211
- The lng/lat metric is raw planar lng/lat and does not wrap longitude at `+/-180`. A point at `179.9` and a point at `-179.9` are treated as about `360` degrees apart, matching the gem's planar `covers_xy?` model. Data that crosses the antimeridian should be cut at `+/-180` before import.
112
+ `Line#nearest_segment(x, y)` and `Ring#nearest_segment(x, y)` expose low-level planar nearest-segment helpers.
212
113
 
213
- `Index` supports radius filters with an rtree bbox prefilter followed by exact distance filtering:
114
+ See [GET_STARTED.md#10-use-nearest-segment-helpers](GET_STARTED.md#10-use-nearest-segment-helpers) and [docs/NEAREST_SEGMENT.md](docs/NEAREST_SEGMENT.md).
214
115
 
215
- ```ruby
216
- index.within_distance_lnglat_meters(lng, lat, radius_m, sort: false)
217
- # => [[id, distance_m], ...]
218
- index.within_distance_ids_lnglat_meters(lng, lat, radius_m)
219
- # => [id, ...]
220
-
221
- index.within_distance_xy(x, y, radius, sort: false)
222
- # => [[id, distance], ...]
223
- index.within_distance_ids_xy(x, y, radius)
224
- # => [id, ...]
225
- ```
116
+ ### Registry helper
226
117
 
227
- `sort: true` sorts filtered `[id, distance]` pairs by ascending distance. The ids variants intentionally do not accept `sort:`. Index kNN / `nearest_ids` is not implemented.
118
+ `TG::Geometry::Registry` is Ruby-level sugar over immutable indexes with safe reload semantics.
228
119
 
229
- Distance radius benchmarks compare `rtree prefilter + exact filter` against a brute-force full index scan. Any ratio from those benchmarks is a prefilter-vs-full-scan result, not a claim that the exact distance calculation itself is hundreds of times faster. The benchmark suite includes tiny-index/full-extent cases where the rtree prefilter may be neutral or slower.
120
+ See [GET_STARTED.md#11-wrap-an-index-in-a-registry](GET_STARTED.md#11-wrap-an-index-in-a-registry).
230
121
 
231
- ## GeoJSON FeatureSource
122
+ ### Optional ActiveRecord integration
232
123
 
233
- `TG::Geometry::FeatureSource` reads GeoJSON `FeatureCollection` sources without `JSON.parse` of the whole document into Ruby Hash/Array objects.
124
+ `TG::Geometry::ActiveRecordType` is an optional read-only convenience type for PostGIS-like geometry columns. It is not required by `tg/geometry`; load it explicitly.
234
125
 
235
- ```ruby
236
- entries = TG::Geometry::FeatureSource.read_entries_file(
237
- "zones.geojson",
238
- id: ["properties", "@id"],
239
- only: [:polygon, :multipolygon]
240
- )
241
-
242
- index = TG::Geometry::Index.build(
243
- entries,
244
- via: :geojson,
245
- strategy: :rtree,
246
- predicate: :covers
247
- )
248
- ```
249
-
250
- For imports that also need raw properties JSON:
251
-
252
- ```ruby
253
- features = TG::Geometry::FeatureSource.read_features_file(
254
- "zones.geojson",
255
- id: ["properties", "@id"],
256
- report: true,
257
- on_invalid: :skip
258
- )
259
-
260
- features[:features].each do |id, geometry_json, properties_json|
261
- # Store geometry_json and parse properties_json in application code if needed.
262
- end
263
- ```
264
-
265
- For direct file-to-index loading:
266
-
267
- ```ruby
268
- index = TG::Geometry::FeatureSource.build_index_file(
269
- "zones.geojson",
270
- id: ["properties", "@id"],
271
- strategy: :rtree,
272
- predicate: :covers
273
- )
274
- ```
275
-
276
- FeatureSource methods are explicit: use `_file` for paths, `_json` for raw content strings, and `_io` for IO objects. There is no path/content auto-detection.
277
-
278
- ## Packed batch point queries
279
-
280
- ```ruby
281
- points = [5.0, 5.0, 25.0, 25.0].pack("d*")
282
- index.covering_ids_batch_packed(points)
283
- # => [:zone_a, :zone_b]
284
- ```
285
-
286
- Input is a Ruby String containing native-endian doubles in `lon, lat` pairs. Length must be a multiple of 16 bytes. Empty string returns `[]`.
287
-
288
- ## Nearest segment
289
-
290
- `Line#nearest_segment(x, y)` and `Ring#nearest_segment(x, y)` return a frozen `TG::Geometry::NearestSegment`:
291
-
292
- ```ruby
293
- nearest = polygon.polygon.exterior_ring.nearest_segment(5, 5)
294
- nearest.segment # => TG::Geometry::Segment
295
- nearest.index # => segment index
296
- nearest.distance # => Float
297
- nearest.point # => [x, y] projection on the segment
298
- ```
299
-
300
- Distance is planar Euclidean distance in input coordinate units. It is not meters unless your input coordinates are already meters. Equal-distance tie-breaks follow tg iteration order and are not API-stable.
301
-
302
- ## Registry helper
303
-
304
- `Registry` is Ruby-level sugar over immutable indexes:
305
-
306
- ```ruby
307
- class DeliveryZones < TG::Geometry::Registry
308
- source do
309
- [
310
- [:zone_a, '{"type":"Polygon","coordinates":[[[0,0],[10,0],[10,10],[0,10],[0,0]]]}']
311
- ]
312
- end
313
-
314
- index_options via: :geojson, strategy: :rtree, predicate: :covers
315
- end
316
-
317
- registry = DeliveryZones.new
318
- registry.reload!
319
- registry.find_covering(5, 5)
320
- ```
321
-
322
- Reload builds a new immutable index first and swaps the reference only after a successful build. Existing readers keep using the previous index safely.
323
-
324
- ## ActiveRecord integration — read-only
325
-
326
- `TG::Geometry::ActiveRecordType` is an optional read-only convenience type for PostGIS columns. It is not required by `tg/geometry`; load it explicitly:
327
-
328
- ```ruby
329
- require "tg/geometry/active_record_type"
330
-
331
- class Zone < ApplicationRecord
332
- attribute :geom, TG::Geometry::ActiveRecordType.new
333
- end
334
-
335
- zone.geom.srid
336
- zone.geom.covers_xy?(lon, lat)
337
- ```
338
-
339
- It can deserialize EWKB bytes, hex EWKB, `\x`-prefixed hex EWKB, GeoJSON, and WKT. Writing `Geom` values is intentionally unsupported in v0.3.0. User applications need `activemodel >= 6.0`; `activerecord` is not a gem dependency.
126
+ See [GET_STARTED.md#12-optional-activerecord-read-only-type](GET_STARTED.md#12-optional-activerecord-read-only-type).
340
127
 
341
128
  ## Memory and concurrency
342
129
 
@@ -346,25 +133,13 @@ The implementation uses explicit allocator pairs and Ruby GC accounting for nati
346
133
 
347
134
  No Ractor support and no universal performance claim are advertised.
348
135
 
349
- ## Benchmarks
136
+ See [docs/MEMORY_OWNERSHIP.md](docs/MEMORY_OWNERSHIP.md) and [docs/CONCURRENCY.md](docs/CONCURRENCY.md).
350
137
 
351
- Benchmark scripts live in `benchmark/`:
138
+ ## Benchmarks
352
139
 
353
- ```bash
354
- bundle exec ruby benchmark/parse_throughput.rb
355
- bundle exec ruby benchmark/flat_vs_rtree.rb
356
- bundle exec ruby benchmark/batch_packed_vs_loop.rb
357
- bundle exec ruby benchmark/objectspace_memsize.rb
358
- bundle exec ruby benchmark/rss_stability.rb
359
- bundle exec ruby benchmark/gvl_threshold.rb
360
- bundle exec ruby benchmark/falcon_concurrency.rb
361
- bundle exec ruby benchmark/feature_source.rb
362
- bundle exec ruby benchmark/geom_query.rb
363
- bundle exec ruby benchmark/nearest_segment.rb
364
- bundle exec ruby benchmark/ewkb_roundtrip.rb
365
- ```
140
+ Benchmark scripts live in `benchmark/`. They are engineering tools, not marketing claims.
366
141
 
367
- The benchmarks are engineering tools, not marketing claims.
142
+ Start with [docs/BENCHMARKING.md](docs/BENCHMARKING.md).
368
143
 
369
144
  ## Limitations
370
145
 
@@ -385,7 +160,21 @@ Not included:
385
160
  - Z/M variants of array constructors;
386
161
  - Public `release_gvl:` option.
387
162
 
388
- TG works in planar XY coordinates. If lon/lat coordinates are passed in, length, area, perimeter, and low-level nearest-segment distances are in input coordinate units, not meters. The explicit `*_lnglat_meters` point-to-geometry APIs are the exception: they return approximate local meters using the query-local frame documented above, not geodesic meters.
163
+ TG works in planar XY coordinates. If lon/lat coordinates are passed in, length, area, perimeter, and low-level nearest-segment distances are in input coordinate units, not meters. The explicit `*_lnglat_meters` point-to-geometry APIs are the exception: they return approximate local meters using a query-local equirectangular frame, not geodesic meters.
164
+
165
+ See [docs/LIMITATIONS.md](docs/LIMITATIONS.md) for the full list.
166
+
167
+ ## Documentation
168
+
169
+ - [GET_STARTED.md](GET_STARTED.md) — runnable examples
170
+ - [docs/ARCHITECTURE.md](docs/ARCHITECTURE.md) — how the C extension is wired
171
+ - [docs/MEMORY_OWNERSHIP.md](docs/MEMORY_OWNERSHIP.md) — allocator pair table
172
+ - [docs/CONCURRENCY.md](docs/CONCURRENCY.md) — threading rules
173
+ - [docs/ERROR_HANDLING.md](docs/ERROR_HANDLING.md) — exception hierarchy
174
+ - [docs/BENCHMARKING.md](docs/BENCHMARKING.md) — running the benchmark scripts
175
+ - [docs/FEATURE_SOURCE.md](docs/FEATURE_SOURCE.md) — GeoJSON FeatureCollection imports
176
+ - [docs/GEOMETRY_QUERIES.md](docs/GEOMETRY_QUERIES.md) — geometry query details
177
+ - [docs/LIMITATIONS.md](docs/LIMITATIONS.md) — non-goals and accuracy boundaries
389
178
 
390
179
  ## Development
391
180
 
@@ -402,6 +191,7 @@ bundle exec rspec spec/batch_packed_spec.rb
402
191
  bundle exec rspec spec/memory_gc_spec.rb
403
192
  bundle exec rspec spec/concurrency_spec.rb
404
193
  bundle exec rspec spec/fuzz_spec.rb
194
+ bundle exec rspec spec/distance_spec.rb
405
195
  ```
406
196
 
407
197
  ## License
@@ -6092,8 +6092,6 @@ static char *fs_safe_tg_error_message(struct tg_geom *geom) {
6092
6092
  char *msg;
6093
6093
  int n;
6094
6094
 
6095
- if (!geom)
6096
- return fs_safe_strdup_cstr("TG geometry allocation failed");
6097
6095
  err = tg_geom_error(geom);
6098
6096
  if (!err)
6099
6097
  return NULL;
@@ -6112,12 +6110,13 @@ static bool fs_safe_validate_geometry(fs_safe_job_t *job, struct json geometry,
6112
6110
  *skipped_out = false;
6113
6111
  struct tg_geom *geom = tg_parse_geojsonn_ix(json_raw(geometry), json_raw_length(geometry),
6114
6112
  job->opts.geometry_index);
6115
- char *message = fs_safe_tg_error_message(geom);
6113
+ char *message;
6116
6114
 
6117
6115
  if (!geom) {
6118
6116
  fs_safe_set_error(job, FS_SAFE_ERROR_NOMEM, "TG geometry allocation failed");
6119
6117
  return false;
6120
6118
  }
6119
+ message = fs_safe_tg_error_message(geom);
6121
6120
 
6122
6121
  if (message) {
6123
6122
  if (job->opts.on_invalid == FS_ON_INVALID_SKIP) {
@@ -6251,11 +6250,11 @@ static bool fs_safe_fill_build(fs_safe_job_t *job, struct json features) {
6251
6250
 
6252
6251
  geom = tg_parse_geojsonn_ix(json_raw(prepared.geometry), json_raw_length(prepared.geometry),
6253
6252
  job->opts.geometry_index);
6254
- message = fs_safe_tg_error_message(geom);
6255
6253
  if (!geom) {
6256
6254
  fs_safe_set_error(job, FS_SAFE_ERROR_NOMEM, "TG geometry allocation failed");
6257
6255
  return false;
6258
6256
  }
6257
+ message = fs_safe_tg_error_message(geom);
6259
6258
  if (message) {
6260
6259
  fs_safe_set_feature_error(job, FS_SAFE_ERROR_PARSE, feature_index,
6261
6260
  fs_safe_json_offset(job, prepared.geometry), message);
@@ -3,20 +3,20 @@ gem=tg_geometry
3
3
  libraries=tg,rtree,json
4
4
  tg_repo=https://github.com/tidwall/tg.git
5
5
  tg_ref=main
6
- tg_commit=caf840504eaab4563280cf4ab16d618f69a23720
6
+ tg_commit=532fc38e1a7bd440339cf83259a9d7983f04ad69
7
7
  tg_target=tg
8
8
  tg_files=tg.c,tg.h,LICENSE,README.md,VERSION
9
- tg_tree_sha256=f8e0d904055c209b2a23a2200456f9374ab95cadb69e61b5721d7f8e2500e705
9
+ tg_tree_sha256=dbf9a49d5ff326974312e94fb39c5687260a2c2b27b10971b24635ed6f79184c
10
10
  rtree_repo=https://github.com/tidwall/rtree.c.git
11
- rtree_ref=v0.5.3
12
- rtree_commit=5717a8a1eb373428ebaae8c1c623f186ec46461f
11
+ rtree_ref=v0.6.1
12
+ rtree_commit=eebc774929f89fd5f4095f7c8835a87f2d2fb73f
13
13
  rtree_target=rtree
14
14
  rtree_files=rtree.c,rtree.h,LICENSE,README.md,VERSION
15
- rtree_tree_sha256=4afc86cbd3abe03730206031a5aff5b8b29d37b055fc356052f6f06e1d1f9a61
15
+ rtree_tree_sha256=6779f4b2c9df8f199907d26c3fba11bc07c6f923d80881577c19f50119e15177
16
16
  json_repo=https://github.com/tidwall/json.c.git
17
17
  json_ref=main
18
- json_commit=4aaf99b5c08e4e1364f97500fe5c5d5a2617b4a6
18
+ json_commit=104d8cbf5d09ceba37d469574cbf5696349bcaf1
19
19
  json_target=json
20
20
  json_files=json.c,json.h,LICENSE,VERSION
21
- json_tree_sha256=059393302de9325ab6ccf79c298234f2b688a59d138103305da317d2d0176fe2
22
- manifest_sha256=6507e2591cadf17ce01616ff54ccbdbb4589ef6130b9edace33cde093d6ae0d5
21
+ json_tree_sha256=764155b839b910f45aaaf0d0881a47570d4ef0919465c5117c7658ab3dac0594
22
+ manifest_sha256=fc8693eafbd0310bfae0d4f8e9d1d68344da7e5169acfb65cdcfc8be33f1b692
@@ -1,3 +1,3 @@
1
1
  repo=https://github.com/tidwall/json.c.git
2
2
  ref=main
3
- commit=4aaf99b5c08e4e1364f97500fe5c5d5a2617b4a6
3
+ commit=104d8cbf5d09ceba37d469574cbf5696349bcaf1