tg_geometry 0.1.0 → 0.3.0

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.
Files changed (91) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +55 -79
  3. data/README.md +168 -184
  4. data/Rakefile +3 -3
  5. data/benchmark/ewkb_roundtrip.rb +29 -0
  6. data/benchmark/falcon_concurrency.rb +1 -1
  7. data/benchmark/feature_source.rb +92 -0
  8. data/benchmark/geom_query.rb +33 -0
  9. data/benchmark/nearest_segment.rb +20 -0
  10. data/docs/ARCHITECTURE.md +29 -107
  11. data/docs/BENCHMARKING.md +20 -1
  12. data/docs/CASUAL_EXAMPLE.md +71 -458
  13. data/docs/CONCURRENCY.md +13 -7
  14. data/docs/ERROR_HANDLING.md +30 -0
  15. data/docs/FEATURE_SOURCE.md +166 -0
  16. data/docs/GEOMETRY_QUERIES.md +23 -0
  17. data/docs/LIMITATIONS.md +17 -54
  18. data/docs/MEMORY_OWNERSHIP.md +20 -2
  19. data/docs/NEAREST_SEGMENT.md +17 -0
  20. data/docs/SRID_AND_EWKB.md +23 -0
  21. data/ext/tg_geometry/extconf.rb +46 -4
  22. data/ext/tg_geometry/tg_geometry_ext.c +3643 -168
  23. data/ext/tg_geometry/tg_geometry_vendor_json.c +17 -0
  24. data/ext/tg_geometry/tg_geometry_vendor_tg.c +3 -0
  25. data/ext/tg_geometry/vendor/.vendored +8 -2
  26. data/ext/tg_geometry/vendor/json/LICENSE +20 -0
  27. data/ext/tg_geometry/vendor/json/VERSION +3 -0
  28. data/ext/tg_geometry/vendor/json/json.c +1024 -0
  29. data/ext/tg_geometry/vendor/json/json.h +207 -0
  30. data/lib/tg/geometry/active_record_source.rb +16 -34
  31. data/lib/tg/geometry/active_record_type.rb +61 -0
  32. data/lib/tg/geometry/registry.rb +16 -67
  33. data/lib/tg/geometry/version.rb +1 -1
  34. data/lib/tg/geometry.rb +85 -0
  35. data/script/vendor_libs.rb +22 -6
  36. data/spec/active_record_type_spec.rb +45 -0
  37. data/spec/{expansion_a_auto_strategy_spec.rb → auto_strategy_spec.rb} +1 -1
  38. data/spec/{block_12_batch_packed_spec.rb → batch_packed_spec.rb} +1 -1
  39. data/spec/{block_20_concurrency_spec.rb → concurrency_spec.rb} +1 -1
  40. data/spec/constructors_spec.rb +104 -0
  41. data/spec/{block_13_error_hardening_spec.rb → error_hardening_spec.rb} +1 -1
  42. data/spec/feature_source_nogvl_spec.rb +51 -0
  43. data/spec/feature_source_spec.rb +268 -0
  44. data/spec/fixtures/feature_source/invalid_geometry_middle.geojson +8 -0
  45. data/spec/fixtures/feature_source/malformed_json.geojson +1 -0
  46. data/spec/fixtures/feature_source/mixed_geometry_types.geojson +8 -0
  47. data/spec/fixtures/feature_source/osm_like_feature_collection.geojson +10 -0
  48. data/spec/fixtures/feature_source/properties_null_missing.geojson +7 -0
  49. data/spec/fixtures/feature_source/simple_feature_collection.geojson +15 -0
  50. data/spec/fixtures/postgis/README.md +16 -0
  51. data/spec/fixtures/postgis/boundary_point_cases.geojson +83 -0
  52. data/spec/fixtures/postgis/multipolygon_large.ewkb +0 -0
  53. data/spec/fixtures/postgis/point_4326.ewkb +0 -0
  54. data/spec/fixtures/postgis/polygon_3857.ewkb +0 -0
  55. data/spec/fixtures/postgis/polygon_4326_simple.ewkb +0 -0
  56. data/spec/fixtures/postgis/polygon_4326_with_hole.ewkb +0 -0
  57. data/spec/{expansion_d_format_coverage_spec.rb → format_coverage_spec.rb} +1 -1
  58. data/spec/{block_20_fuzz_spec.rb → fuzz_spec.rb} +1 -1
  59. data/spec/{block_4_geom_api_spec.rb → geom_api_spec.rb} +1 -1
  60. data/spec/{block_3_geom_parse_spec.rb → geom_parse_spec.rb} +1 -1
  61. data/spec/{block_8_index_borrowed_geometry_spec.rb → index_borrowed_geometry_spec.rb} +1 -1
  62. data/spec/{block_6_index_build_spec.rb → index_build_spec.rb} +2 -2
  63. data/spec/{block_9_flat_query_spec.rb → index_flat_query_spec.rb} +1 -1
  64. data/spec/index_geom_query_spec.rb +68 -0
  65. data/spec/{block_7_index_owned_geometry_spec.rb → index_owned_geometry_spec.rb} +1 -1
  66. data/spec/{block_10_rtree_strategy_spec.rb → index_rtree_accounting_spec.rb} +1 -1
  67. data/spec/{block_11_rtree_order_spec.rb → index_rtree_order_spec.rb} +1 -1
  68. data/spec/keyword_validation_spec.rb +31 -0
  69. data/spec/{block_1_skeleton_spec.rb → load_and_errors_spec.rb} +1 -1
  70. data/spec/{expansion_e_low_level_geometry_spec.rb → low_level_geometry_spec.rb} +1 -1
  71. data/spec/{block_14_memory_gc_hardening_spec.rb → memory_gc_spec.rb} +1 -1
  72. data/spec/nearest_segment_spec.rb +62 -0
  73. data/spec/postgis_fixtures_spec.rb +68 -0
  74. data/spec/{expansion_i_ractor_spec.rb → ractor_spec.rb} +1 -1
  75. data/spec/{block_5_rect_api_spec.rb → rect_api_spec.rb} +1 -1
  76. data/spec/{expansion_b_registry_spec.rb → registry_spec.rb} +1 -1
  77. data/spec/srid_spec.rb +43 -0
  78. data/spec/{expansion_j_full_tg_api_coverage_spec.rb → tg_api_coverage_spec.rb} +1 -1
  79. data/spec/to_ewkb_spec.rb +37 -0
  80. data/spec/{block_2_vendor_spec.rb → vendor_sources_spec.rb} +4 -4
  81. metadata +87 -38
  82. data/docs/ACTIVE_RECORD.md +0 -26
  83. data/docs/AUTO_STRATEGY.md +0 -15
  84. data/docs/EXPANSION_E_TO_H_STATUS.md +0 -51
  85. data/docs/FORMAT_COVERAGE.md +0 -23
  86. data/docs/FULL_TG_API_COVERAGE.md +0 -109
  87. data/docs/LOW_LEVEL_GEOMETRY.md +0 -121
  88. data/docs/RACTOR.md +0 -40
  89. data/docs/REGISTRY.md +0 -37
  90. data/docs/RELEASE_CHECKLIST.md +0 -39
  91. /data/spec/{expansion_c_active_record_source_spec.rb → active_record_source_spec.rb} +0 -0
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 01f50a6c3b237bc983741eaf46a6440220c023dde0d91e8a1e64debb0dd8cf9e
4
- data.tar.gz: e07214e6f201842029f415e1dc4c39f77cd9c003187cc9068013f72300f5398a
3
+ metadata.gz: fdc6ab78992bee6ba1708b13adfdb0e4eec26fef04c4fc8eae87df21fbfed5fa
4
+ data.tar.gz: ada3eff77de10cb101cbedbd35e2d40e119fe37c5a09003afe9df1ab266827fe
5
5
  SHA512:
6
- metadata.gz: c80efd671c29339c6023a4b7b616eb0f4eba73513aecea5dea1f239797a8c8c3c4b9a9fd12a1bc69d359a599b3dfc3c4891a8a2073d29e5525083538379b3dea
7
- data.tar.gz: '0508a2a6f736791048212c214b75533131c83d0e38029a9b2a13d7860991f882868faeb6237f6305645fc0b4a4819ac0fd21b2c3968006e2a85d2b736425a7ff'
6
+ metadata.gz: 87200faec4b6d0442eab7df12f768807193aed725bb75b25502a64cc5b2e24e97f2f9d98cf9254c54e3f62d9b0481f39dc2287818f69a740e9728a33e97d9f0b
7
+ data.tar.gz: 3e14f4563facef36fc109e1c8006b707502bb384f67c1c27da05faa7dcec7b7a7970d82a22d39ac99c00595c74944c7a6467fa81a5cdd1d14129df70f4d3675a
data/CHANGELOG.md CHANGED
@@ -1,103 +1,79 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.3.0 - 27.05.2026
4
+
5
+ ### Added
6
+
7
+ - `TG::Geometry.line_string`, `TG::Geometry.polygon`, and `TG::Geometry.multi_polygon` constructors from Ruby arrays.
8
+ - `TG::Geometry::Geom#srid` with automatic EWKB SRID extraction in `parse_wkb` and `parse_hex`.
9
+ - `TG::Geometry::Geom#to_ewkb` writer with explicit `srid:` override.
10
+ - `TG::Geometry::Index#intersecting_geom_ids`, `#covering_geom_ids`, and `#containing_geom_ids`.
11
+ - `TG::Geometry::Line#nearest_segment` and `TG::Geometry::Ring#nearest_segment` with `TG::Geometry::NearestSegment` result objects.
12
+ - `TG::Geometry::ActiveRecordType` read-only optional require.
13
+ - PostGIS/EWKB fixture suite in `spec/fixtures/postgis/`.
14
+
15
+ ### Fixed
16
+
17
+ - Reject unsupported/unknown keywords on v0.3.0 APIs instead of silently ignoring them, including non-goals such as `release_gvl:`, `parse_wkb(srid:)`, and `parse_wkb(ewkb:)`.
18
+ - Harden constructor cleanup paths so intermediate C `tg_ring` / `tg_poly` objects are released when Ruby exceptions occur during nested polygon construction.
19
+ - Keep geometry-index query collection in a C-only status-returning phase before Ruby result materialization, preserving the intended no-GVL-safe internal shape.
20
+
21
+ ### Clarified
22
+
23
+ - `Index.build(predicate:)` affects only legacy point query methods: `find_covering`, `covering_ids(x, y)`, and `covering_ids_batch_packed`.
24
+
25
+ ### Not included
26
+
27
+ - Geodesic / Haversine distance.
28
+ - Projection / reprojection.
29
+ - Buffer / union / difference / convex hull.
30
+ - Index nearest_ids (KNN).
31
+ - GeoBIN bbox helpers.
32
+ - Streaming FeatureSource.
33
+ - Index serialization / mmap.
34
+ - Ractor support.
35
+ - Windows / JRuby.
36
+ - Write-side ActiveRecordType / AR scopes / migrations.
37
+ - Z/M variants of constructors.
38
+ - Public `release_gvl:` option.
39
+
3
40
  ## 0.1.0 - unreleased
4
41
 
5
- Initial release-core implementation for `tg_geometry`.
42
+ Initial public release candidate for `tg_geometry`.
6
43
 
7
44
  ### Added
8
45
 
9
46
  - Canonical public require path: `require "tg/geometry"`.
10
47
  - Public namespace: `TG::Geometry`.
11
48
  - Native extension build through `ext/tg_geometry/extconf.rb`.
12
- - Vendored `tidwall/tg` and `tidwall/rtree.c` sources with pinned `VERSION` files and upstream license files.
49
+ - Vendored `tidwall/tg`, `tidwall/rtree.c`, and `tidwall/json.c` sources with pinned `VERSION` files and upstream license files.
13
50
  - Error classes:
14
51
  - `TG::Geometry::Error`
15
52
  - `TG::Geometry::ParseError`
16
53
  - `TG::Geometry::ArgumentError < ::ArgumentError`
17
54
  - `TG::Geometry::FrozenIndexError`
18
55
  - Immutable `TG::Geometry::Geom` parsing for GeoJSON, WKT, WKB, Hex, GeoBIN, and auto format detection.
19
- - `TG::Geometry::Geom` methods:
20
- - `#type`
21
- - `#bbox`
22
- - `#covers_xy?`
23
- - `#contains?`
24
- - `#intersects?`
25
- - `#to_geojson`
26
- - `#to_wkt`
27
- - `#to_wkb`
28
56
  - Immutable `TG::Geometry::Rect` API.
29
57
  - Immutable `TG::Geometry::Index.build` with strict `[[id, object], ...]` entry format.
30
- - Index ingestion modes:
31
- - `via: :geom` borrowed geometry with `geom_owner` lifetime protection;
32
- - `via: :geojson` owned geometry;
33
- - `via: :wkb` owned geometry.
34
- - Index strategies:
35
- - `:flat`
36
- - `:rtree`
58
+ - Index ingestion modes: `via: :geom`, `via: :geojson`, and `via: :wkb`.
59
+ - Index strategies: `:flat` and `:rtree`.
37
60
  - Deterministic insertion-order results for flat and rtree queries.
38
- - Exact rtree memory accounting through a custom malloc/free allocator with headers.
61
+ - Exact rtree memory accounting through a custom malloc/free allocator with allocation headers.
39
62
  - Native-endian packed point batch API: `TG::Geometry::Index#covering_ids_batch_packed`.
40
- - Debug-only test hooks under `TG_DEBUG_TEST=1` for allocation failure simulation and byte counter inspection.
41
- - Block 14 memory/GC/compaction hardening specs.
42
- - Benchmark harnesses:
43
- - `benchmark/parse_throughput.rb`
44
- - `benchmark/gvl_threshold.rb`
45
- - `benchmark/flat_vs_rtree.rb`
46
- - `benchmark/batch_packed_vs_loop.rb`
47
- - `benchmark/falcon_concurrency.rb`
48
- - `benchmark/objectspace_memsize.rb`
49
- - `benchmark/rss_stability.rb`
50
- - Documentation:
51
- - `docs/ARCHITECTURE.md`
52
- - `docs/MEMORY_OWNERSHIP.md`
53
- - `docs/CONCURRENCY.md`
54
- - `docs/ERROR_HANDLING.md`
55
- - `docs/BENCHMARKING.md`
56
- - `docs/LIMITATIONS.md`
57
- - `docs/RELEASE_CHECKLIST.md`
63
+ - GeoJSON FeatureSource APIs for reading FeatureCollection entries/features and building an Index directly from file, JSON string, or IO.
64
+ - FeatureSource bulk execution now runs file read, JSON traversal, and TG geometry parsing in a C-only no-GVL phase before Ruby materialization / Index ownership transfer. The implementation uses Ruby VM no-GVL APIs only: `RB_NOGVL_OFFLOAD_SAFE` when available, otherwise `rb_thread_call_without_gvl`. It does not use a manual Fiber scheduler block/unblock worker path.
65
+ - Ruby-level `TG::Geometry::Registry` reload helper.
66
+ - Optional `TG::Geometry::ActiveRecordSource` helper without a Rails runtime dependency.
67
+ - Benchmark scripts under `benchmark/`.
68
+ - Minimal documentation under `docs/`.
58
69
 
59
70
  ### Not included
60
71
 
61
- - `strategy: :auto` is not part of the release-core contract; it is tracked as Expansion Block A below.
62
- - No Ractor support claim.
63
- - No no-GVL execution.
64
- - No full GIS, routing, geocoding, projections, geodesic helpers, nearest POI index, or result-geometry overlay operations.
65
- - No public performance claims until benchmark results are produced by this gem.
66
-
67
- ### OPEN QUESTION
68
-
69
- - Final ASAN setup requires Roman approval before replacing the placeholder CI job.
70
- - Final Valgrind setup requires Roman approval before replacing the placeholder CI job.
71
-
72
- ## Unreleased
73
-
74
- ### Added
75
-
76
- - Expansion Block A status: `strategy: :auto` remains postponed for the first public release; explicit `:flat` / `:rtree` strategies are required.
77
- - Expansion Block B: `TG::Geometry::Registry` Ruby helper for immutable Index reload/swap workflows.
78
- - Expansion Block C: optional `TG::Geometry::ActiveRecordSource` helper that converts relation-like records into strict `[[id, object], ...]` entries without adding a Rails dependency.
79
- - Expansion Block D: `TG::Geometry.parse_hex`, `TG::Geometry.parse_geobin`, `TG::Geometry::Geom#to_hex`, `#to_geobin`, and `#extra_json`.
80
- - Expansion Block E: read-only borrowed low-level wrappers: `TG::Geometry::Line`, `TG::Geometry::Ring`, `TG::Geometry::Polygon`, plus `TG::Geometry::Geom#point`, `#line`, and `#polygon`.
81
- - Expansion Block I: Ractor unsupported-boundary investigation documented in `docs/RACTOR.md` with specs asserting native wrappers are not treated as shareable Ractor objects.
82
- - Expansion Block J grouped API coverage:
83
- - safe point and empty geometry constructors;
84
- - additional `TG::Geometry::Geom` predicates;
85
- - geometry metadata and Z/M read accessors;
86
- - MultiPoint/MultiLineString/MultiPolygon and GeometryCollection accessors;
87
- - borrowed child `TG::Geometry::Geom` wrappers with `geom_owner`;
88
- - value `TG::Geometry::Segment` wrappers from Line/Ring segment accessors.
89
-
90
- ### Fixed
91
-
92
- - Corrected `benchmark/gvl_threshold.rb` so each target size uses a valid WKT payload near that size instead of repeatedly benchmarking the same tiny polygon.
93
-
94
- ### Documentation
95
-
96
- - Added docs for Registry, ActiveRecord source helper, additional format coverage, low-level borrowed geometry wrappers, Ractor unsupported-boundary status, grouped full TG API coverage, Auto Strategy postponed status, and Expansion Blocks E–H status.
97
-
98
- ### OPEN QUESTION
72
+ - `strategy: :auto`.
73
+ - Ractor support claim.
74
+ - no general no-GVL claim for short query/parse/write paths.
75
+ - Full GIS functionality such as routing, projections, geodesics, overlay result geometries, or nearest POI search.
76
+ - Public callback/search APIs.
77
+ - Universal performance claims.
99
78
 
100
- - Expansion Block F callback/search APIs remain blocked until a callback safety contract, exception semantics, GVL rules, and callback overhead benchmarks are approved.
101
- - Expansion Block G no-allocation point query optimization remains blocked until boundary/hole-boundary equivalence tests and benchmarks prove it preserves `:covers` / `:contains` semantics.
102
- - Expansion Block H geodesic/projection helpers remain blocked until an explicit optional dependency/API decision is approved.
103
- - Remaining Expansion Block J scope such as Line/Ring/Polygon constructors, callback/search APIs, nearest segment APIs, global environment configuration, and allocator override APIs remains blocked until separate ownership/thread-safety contracts are approved.
79
+ - Suppressed known GCC diagnostics from vendored tidwall/tg wrapper on Linux CI without muting warnings from tg_geometry's own C code.
data/README.md CHANGED
@@ -1,39 +1,17 @@
1
1
  # tg_geometry
2
2
 
3
- `tg_geometry` is a Ruby C extension around the vendored `tidwall/tg` geometry
4
- library and `tidwall/rtree.c`.
3
+ `tg_geometry` is a Ruby C extension around vendored `tidwall/tg`, `tidwall/rtree.c`, and pinned `tidwall/json.c`.
5
4
 
6
- It exposes the public Ruby namespace `TG::Geometry` and the canonical require
7
- path:
5
+ It exposes the public Ruby namespace `TG::Geometry`:
8
6
 
9
7
  ```ruby
10
8
  require "tg/geometry"
11
9
  ```
12
10
 
13
- The gem targets fast in-process planar geometry parsing, predicates,
14
- format conversion, and geofencing-oriented immutable indexes. It does not try
15
- to be a full GIS system.
16
-
17
- ## Status
18
-
19
- This repository is prepared as a first public release candidate with an
20
- expanded API surface:
21
-
22
- - release-core `Geom`, `Rect`, and immutable `Index` APIs;
23
- - expanded format coverage for Hex and GeoBIN;
24
- - read-only borrowed wrappers for lower-level TG geometry components;
25
- - `Registry` reload/swap sugar;
26
- - optional ActiveRecord-style source helpers that do not add a Rails runtime
27
- dependency.
28
-
29
- `strategy: :auto`, Ractor support, callback/search APIs, no-allocation point
30
- query optimization, geodesic helpers, projections, and no-GVL execution are not
31
- claimed in this release.
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.
32
12
 
33
13
  ## Installation
34
14
 
35
- Add this line to your application's Gemfile:
36
-
37
15
  ```ruby
38
16
  gem "tg_geometry"
39
17
  ```
@@ -44,17 +22,13 @@ Then run:
44
22
  bundle install
45
23
  ```
46
24
 
47
- The extension is built from vendored C sources. There is no GEOS, PostGIS,
48
- PROJ, GDAL, system TG, or system rtree dependency.
25
+ The extension builds from vendored C sources. It does not require GEOS, PostGIS, PROJ, GDAL, system TG, or system rtree.
49
26
 
50
- Supported first-release platforms are Linux and macOS on x86_64/aarch64.
51
- Windows is not supported in this release.
27
+ Supported platforms: Linux and macOS on x86_64/aarch64. Windows is not supported for the first release.
52
28
 
53
- ## Basic parsing and predicates
29
+ ## Parsing and predicates
54
30
 
55
31
  ```ruby
56
- require "tg/geometry"
57
-
58
32
  zone = TG::Geometry.parse_geojson(<<~JSON)
59
33
  {
60
34
  "type": "Polygon",
@@ -67,16 +41,11 @@ zone.type # => :polygon
67
41
  zone.covers_xy?(5, 5) # => true
68
42
  zone.covers_xy?(0, 0) # => true, boundary is covered
69
43
  zone.bbox # => #<TG::Geometry::Rect ...>
70
-
71
- wkt = zone.to_wkt
72
- wkb = zone.to_wkb
44
+ zone.to_wkt
45
+ zone.to_wkb
73
46
  ```
74
47
 
75
- `TG::Geometry::Geom` objects are immutable. They cannot be manually allocated or
76
- manually freed from Ruby. Native memory is released by Ruby GC through the typed
77
- data wrapper.
78
-
79
- ## Parse API
48
+ Parse shortcuts:
80
49
 
81
50
  ```ruby
82
51
  TG::Geometry.parse(str, format: :auto, index: :ystripes)
@@ -87,62 +56,50 @@ TG::Geometry.parse_hex(str, index: :ystripes)
87
56
  TG::Geometry.parse_geobin(bytes, index: :ystripes)
88
57
  ```
89
58
 
90
- Accepted `format:` values for `parse` are:
59
+ `TG::Geometry::Geom` objects are immutable and cannot be manually allocated or manually freed from Ruby.
91
60
 
92
- - `:auto`
93
- - `:geojson`
94
- - `:wkt`
95
- - `:wkb`
96
- - `:hex`
97
- - `:geobin`
61
+ ## Constructors
98
62
 
99
- Accepted TG internal polygon index values are:
63
+ Construct simple planar geometries directly from Ruby arrays without parsing strings or bytes:
100
64
 
101
- - `:default`
102
- - `:none`
103
- - `:natural`
104
- - `:ystripes`
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
+ ```
105
78
 
106
- Parse failures raise `TG::Geometry::ParseError`. Invalid options raise
107
- `TG::Geometry::ArgumentError`, which inherits from Ruby's `::ArgumentError`.
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.
108
80
 
109
- ## Geom API
81
+ ## SRID and EWKB
110
82
 
111
- Release-core methods:
83
+ `parse_wkb` and `parse_hex` preserve EWKB SRID metadata when the EWKB SRID flag is present:
112
84
 
113
85
  ```ruby
114
- geom.type
115
- geom.bbox
116
- geom.covers_xy?(x, y)
117
- geom.contains?(other_geom)
118
- geom.intersects?(other_geom)
119
- geom.to_geojson
120
- geom.to_wkt
121
- geom.to_wkb
86
+ geom = TG::Geometry.parse_wkb(postgis_bytea)
87
+ geom.srid # => 4326, 3857, 0, or nil for plain WKB
122
88
  ```
123
89
 
124
- Expanded methods include additional predicates, format writers, metadata
125
- accessors, and read-only borrowed child wrappers. See:
90
+ `to_wkb` always writes plain WKB. Use `to_ewkb` when a PostGIS-compatible SRID-bearing payload is required:
126
91
 
127
- - `docs/FORMAT_COVERAGE.md`
128
- - `docs/LOW_LEVEL_GEOMETRY.md`
129
- - `docs/FULL_TG_API_COVERAGE.md`
92
+ ```ruby
93
+ ewkb = geom.to_ewkb # uses geom.srid
94
+ ewkb = geom.to_ewkb(srid: 4326) # explicit override
95
+ ```
130
96
 
131
- For point predicates, this release prioritizes exact `covers` / `contains`
132
- semantics over the fastest possible no-allocation path. Query methods construct
133
- a temporary TG point geometry and free it before returning. A future optimized
134
- point path requires boundary and hole-boundary equivalence tests plus benchmark
135
- proof.
97
+ SRID is metadata only. `tg_geometry` does not check SRID compatibility, transform coordinates, or calculate geodesic distances.
136
98
 
137
- ## Rect API
99
+ ## Rect
138
100
 
139
101
  ```ruby
140
102
  rect = TG::Geometry::Rect.new(0, 0, 10, 10)
141
-
142
- rect.min_x
143
- rect.min_y
144
- rect.max_x
145
- rect.max_y
146
103
  rect.center # => [5.0, 5.0]
147
104
  rect.contains_point?(5, 5) # => true
148
105
  rect.intersects?(other_rect)
@@ -150,16 +107,10 @@ rect.expand_to_include(other_rect)
150
107
  rect.expand_to_include_point(x, y)
151
108
  ```
152
109
 
153
- `Rect` rejects non-finite coordinates and invalid coordinate order. It is frozen
154
- after construction.
155
-
156
- There is intentionally no first-release `Rect#contains?` method because the name
157
- is ambiguous. Use `contains_point?`.
110
+ `Rect` rejects non-finite coordinates and invalid coordinate order. It is frozen after construction.
158
111
 
159
112
  ## Immutable Index
160
113
 
161
- `TG::Geometry::Index` is built once and then read-only forever.
162
-
163
114
  ```ruby
164
115
  entries = [
165
116
  [:zone_a, '{"type":"Polygon","coordinates":[[[0,0],[10,0],[10,10],[0,10],[0,0]]]}'],
@@ -183,7 +134,7 @@ index.covering_ids(5, 5) # => [:zone_a]
183
134
  index.intersecting_rect(0, 0, 25, 25)
184
135
  ```
185
136
 
186
- Accepted input format:
137
+ Accepted input shape:
187
138
 
188
139
  ```ruby
189
140
  [[id1, object1], [id2, object2], ...]
@@ -194,17 +145,13 @@ Rules:
194
145
  - `entries` must be an Array.
195
146
  - Every entry must be a two-element Array.
196
147
  - `id` may be any Ruby object except `nil`.
197
- - `false` ids are accepted, but discouraged because `find_covering` uses `nil`
198
- for no match.
199
148
  - Duplicate ids are allowed.
200
- - Returned ids are the same Ruby objects stored in the index; they are not
201
- copied, frozen, stringified, or duplicated.
149
+ - Returned ids are the same Ruby objects stored in the index.
202
150
  - Result order is insertion order for both `:flat` and `:rtree`.
203
151
 
204
152
  Accepted `via:` modes:
205
153
 
206
- - `:geom` — borrow an existing `TG::Geometry::Geom`; the index marks the owner
207
- wrapper so the borrowed native pointer remains valid.
154
+ - `:geom` — borrow an existing `TG::Geometry::Geom` and keep its owner alive.
208
155
  - `:geojson` — parse and own native TG geometries inside the index.
209
156
  - `:wkb` — parse and own native TG geometries inside the index.
210
157
 
@@ -213,21 +160,82 @@ Accepted strategies:
213
160
  - `:flat`
214
161
  - `:rtree`
215
162
 
216
- `strategy: :auto` is not exposed in this release. The benchmark output does not
217
- support a single universal threshold: flat scan may win for early insertion-order
218
- hits or heavily overlapping datasets, while rtree may win for misses, later hits,
219
- or selective rectangle queries. Choose the strategy explicitly and benchmark on
220
- your own data.
221
-
222
163
  Accepted predicates:
223
164
 
224
165
  - `:covers` — default for geofencing; boundary points are included.
225
- - `:contains` — stricter OGC-style containment semantics.
166
+ - `:contains` — stricter containment semantics.
226
167
 
227
- ## Packed batch point queries
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.
169
+
170
+ `strategy: :auto` is intentionally not exposed. Choose the strategy explicitly and benchmark on your own data.
171
+
172
+ ## Geometry-based index queries
173
+
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 |
188
+
189
+ Results are ids only and preserve insertion order. Duplicate ids remain possible if duplicate ids were inserted.
190
+
191
+ ## GeoJSON FeatureSource
192
+
193
+ `TG::Geometry::FeatureSource` reads GeoJSON `FeatureCollection` sources without `JSON.parse` of the whole document into Ruby Hash/Array objects.
194
+
195
+ ```ruby
196
+ entries = TG::Geometry::FeatureSource.read_entries_file(
197
+ "zones.geojson",
198
+ id: ["properties", "@id"],
199
+ only: [:polygon, :multipolygon]
200
+ )
201
+
202
+ index = TG::Geometry::Index.build(
203
+ entries,
204
+ via: :geojson,
205
+ strategy: :rtree,
206
+ predicate: :covers
207
+ )
208
+ ```
209
+
210
+ For imports that also need raw properties JSON:
211
+
212
+ ```ruby
213
+ features = TG::Geometry::FeatureSource.read_features_file(
214
+ "zones.geojson",
215
+ id: ["properties", "@id"],
216
+ report: true,
217
+ on_invalid: :skip
218
+ )
219
+
220
+ features[:features].each do |id, geometry_json, properties_json|
221
+ # Store geometry_json and parse properties_json in application code if needed.
222
+ end
223
+ ```
228
224
 
229
- For high-throughput same-process point lookups, the index supports a packed
230
- native-endian double input format:
225
+ For direct file-to-index loading:
226
+
227
+ ```ruby
228
+ index = TG::Geometry::FeatureSource.build_index_file(
229
+ "zones.geojson",
230
+ id: ["properties", "@id"],
231
+ strategy: :rtree,
232
+ predicate: :covers
233
+ )
234
+ ```
235
+
236
+ 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.
237
+
238
+ ## Packed batch point queries
231
239
 
232
240
  ```ruby
233
241
  points = [5.0, 5.0, 25.0, 25.0].pack("d*")
@@ -235,18 +243,23 @@ index.covering_ids_batch_packed(points)
235
243
  # => [:zone_a, :zone_b]
236
244
  ```
237
245
 
238
- Input format:
246
+ Input is a Ruby String containing native-endian doubles in `lon, lat` pairs. Length must be a multiple of 16 bytes. Empty string returns `[]`.
247
+
248
+ ## Nearest segment
249
+
250
+ `Line#nearest_segment(x, y)` and `Ring#nearest_segment(x, y)` return a frozen `TG::Geometry::NearestSegment`:
239
251
 
240
- - Ruby String treated as raw bytes.
241
- - Native-endian doubles.
242
- - Pairs of `lon, lat`.
243
- - Length must be a multiple of 16 bytes.
244
- - Empty string returns `[]`.
252
+ ```ruby
253
+ nearest = polygon.polygon.exterior_ring.nearest_segment(5, 5)
254
+ nearest.segment # => TG::Geometry::Segment
255
+ nearest.index # => segment index
256
+ nearest.distance # => Float
257
+ nearest.point # => [x, y] projection on the segment
258
+ ```
245
259
 
246
- This format is intentionally native-endian for same-process speed and simplicity.
247
- Do not use it as a cross-platform serialized file format.
260
+ 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.
248
261
 
249
- ## Registry reload pattern
262
+ ## Registry helper
250
263
 
251
264
  `Registry` is Ruby-level sugar over immutable indexes:
252
265
 
@@ -266,53 +279,32 @@ registry.reload!
266
279
  registry.find_covering(5, 5)
267
280
  ```
268
281
 
269
- Reload builds a new full immutable index first and swaps the reference only after
270
- successful build:
271
-
272
- ```ruby
273
- new_index = TG::Geometry::Index.build(entries, via: :geojson, strategy: :rtree)
274
- @index = new_index
275
- ```
276
-
277
- Old indexes remain alive while existing readers hold references to them. There
278
- is no in-place mutation, no public `add`, `delete`, `clear`, or `rebuild!` API on
279
- `Index`.
282
+ Reload builds a new immutable index first and swaps the reference only after a successful build. Existing readers keep using the previous index safely.
280
283
 
281
- See `docs/REGISTRY.md` and `docs/ACTIVE_RECORD.md` for the expanded helpers.
284
+ ## ActiveRecord integration — read-only
282
285
 
283
- ## Memory ownership model
286
+ `TG::Geometry::ActiveRecordType` is an optional read-only convenience type for PostGIS columns. It is not required by `tg/geometry`; load it explicitly:
284
287
 
285
- The implementation uses explicit allocator pairs and GC accounting:
286
-
287
- | Resource | Allocator | Deallocator | Owner |
288
- |---|---|---|---|
289
- | `tg_geom_wrapper_t` | `TypedData_Make_Struct` / Ruby allocator | `ruby_xfree` | Ruby `Geom` object |
290
- | TG geometry in `Geom` | TG parser/constructor | `tg_geom_free` | `Geom` wrapper |
291
- | `tg_index_t` | `TypedData_Make_Struct` / Ruby allocator | `ruby_xfree` | Ruby `Index` object |
292
- | Index entries array | `calloc` | `free` | `Index` |
293
- | TG geometry via `:geojson` / `:wkb` | TG parser | `tg_geom_free` | `Index` |
294
- | TG geometry via `:geom` | Existing `Geom` wrapper | Existing `Geom` wrapper | Borrowed by `Index` through `geom_owner` |
295
- | rtree internals | custom `tg_rtree_malloc` with header | custom `tg_rtree_free` | rtree / `Index` accounting |
296
- | Ruby ids | Ruby VM | Ruby GC | Marked and compacted by `Index` |
288
+ ```ruby
289
+ require "tg/geometry/active_record_type"
297
290
 
298
- `ObjectSpace.memsize_of(index)` includes entries, owned TG geometries, and exact
299
- rtree allocation bytes. Borrowed geometries are not double-counted by the index.
291
+ class Zone < ApplicationRecord
292
+ attribute :geom, TG::Geometry::ActiveRecordType.new
293
+ end
300
294
 
301
- See `docs/MEMORY_OWNERSHIP.md` for the full table and cleanup rules.
295
+ zone.geom.srid
296
+ zone.geom.covers_xy?(lon, lat)
297
+ ```
302
298
 
303
- ## Concurrency model
299
+ 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.
304
300
 
305
- `Index` and `Geom` are immutable after construction. Concurrent read-only use
306
- from normal Ruby threads is supported by design and covered by tests.
301
+ ## Memory and concurrency
307
302
 
308
- The first release keeps GVL for parse, write, query, batch, and rtree build/free
309
- paths. This is intentional: the rtree allocator calls Ruby GC accounting APIs,
310
- and no-GVL execution would require separate input-copying and allocator-accounting
311
- design.
303
+ The implementation uses explicit allocator pairs and Ruby GC accounting for native memory. `ObjectSpace.memsize_of(index)` includes entries, owned TG geometries, and exact rtree allocation bytes. Borrowed geometries are not double-counted by the index.
312
304
 
313
- No Ractor support is claimed.
305
+ `Index` and `Geom` are immutable after construction. Concurrent read-only use from normal Ruby threads is supported. Short query/parse/write paths keep the GVL. FeatureSource bulk loading uses a C-only no-GVL heavy phase for file read, JSON traversal, and TG geometry parsing, then reacquires the GVL to create Ruby objects or transfer ownership into the final Index. On Ruby versions that expose `RB_NOGVL_OFFLOAD_SAFE`, that no-GVL phase is marked offload-safe for the Ruby VM. On older Rubies it still releases the GVL for other Ruby threads, but no explicit Fiber scheduler friendliness is claimed.
314
306
 
315
- See `docs/CONCURRENCY.md` and `docs/RACTOR.md`.
307
+ No Ractor support and no universal performance claim are advertised.
316
308
 
317
309
  ## Benchmarks
318
310
 
@@ -326,37 +318,34 @@ bundle exec ruby benchmark/objectspace_memsize.rb
326
318
  bundle exec ruby benchmark/rss_stability.rb
327
319
  bundle exec ruby benchmark/gvl_threshold.rb
328
320
  bundle exec ruby benchmark/falcon_concurrency.rb
321
+ bundle exec ruby benchmark/feature_source.rb
322
+ bundle exec ruby benchmark/geom_query.rb
323
+ bundle exec ruby benchmark/nearest_segment.rb
324
+ bundle exec ruby benchmark/ewkb_roundtrip.rb
329
325
  ```
330
326
 
331
- By default, benchmarks use a fast local matrix. Set `TGEOMETRY_BENCH_FULL=1` for
332
- the larger matrix where supported.
333
-
334
- The repository benchmarks are engineering tools, not universal marketing claims.
335
- Do not copy upstream TG C benchmark numbers as Ruby gem performance claims.
327
+ The benchmarks are engineering tools, not marketing claims.
336
328
 
337
329
  ## Limitations
338
330
 
339
331
  `tg_geometry` is not a full GIS system.
340
332
 
341
- Not included in this release:
342
-
343
- - geocoding;
344
- - routing;
345
- - projections;
346
- - geodesic distance/area;
347
- - buffer / union / difference / overlay result geometry operations;
348
- - nearest POI index;
349
- - Rails dependency in the core extension;
350
- - Redis or external service dependency;
351
- - public callback/search APIs;
352
- - Ractor support claim;
353
- - no-GVL execution claim;
354
- - universal `:auto` strategy.
355
-
356
- TG works in planar XY coordinates. If lon/lat coordinates are passed in, length,
357
- area, and perimeter-style values are in input coordinate units, not meters.
358
- Use PostGIS, GEOS, PROJ, or other GIS tooling when full GIS functionality is
359
- needed.
333
+ Not included:
334
+
335
+ - Geodesic / Haversine distance;
336
+ - Projection / reprojection;
337
+ - Buffer / union / difference / convex hull;
338
+ - Index nearest_ids (KNN);
339
+ - GeoBIN bbox helpers;
340
+ - Streaming FeatureSource;
341
+ - Index serialization / mmap;
342
+ - Ractor support;
343
+ - Windows / JRuby;
344
+ - Write-side ActiveRecordType / AR scopes / migrations;
345
+ - Z/M variants of array constructors;
346
+ - Public `release_gvl:` option.
347
+
348
+ TG works in planar XY coordinates. If lon/lat coordinates are passed in, length, area, perimeter, and nearest-segment distances are in input coordinate units, not meters.
360
349
 
361
350
  ## Development
362
351
 
@@ -369,17 +358,12 @@ bundle exec rake spec
369
358
  Useful targeted checks:
370
359
 
371
360
  ```bash
372
- bundle exec rspec spec/block_12_batch_packed_spec.rb
373
- bundle exec rspec spec/block_14_memory_gc_hardening_spec.rb
374
- bundle exec rspec spec/block_20_concurrency_spec.rb
375
- bundle exec rspec spec/block_20_fuzz_spec.rb
361
+ bundle exec rspec spec/batch_packed_spec.rb
362
+ bundle exec rspec spec/memory_gc_spec.rb
363
+ bundle exec rspec spec/concurrency_spec.rb
364
+ bundle exec rspec spec/fuzz_spec.rb
376
365
  ```
377
366
 
378
- Memory-tool CI jobs for ASAN and Valgrind are intentionally left as OPEN QUESTION
379
- placeholders until the exact setup is approved. Do not replace them with guessed
380
- configuration.
381
-
382
367
  ## License
383
368
 
384
- MIT. Vendored upstream license files for `tidwall/tg` and `tidwall/rtree.c` are
385
- included under `ext/tg_geometry/vendor/`.
369
+ MIT. Vendored upstream license files for `tidwall/tg`, `tidwall/rtree.c`, and `tidwall/json.c` are included under `ext/tg_geometry/vendor/`.