geodetic 0.8.0 → 0.8.5

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: 542167ec9e7b27122d31d5287f967481dc9d0d4b293eaaaa349ca4203e2eb302
4
- data.tar.gz: f544332a931f775df18ce4e0d2ed78e4d2619bbb946df6d287c8bc3a798d6c9f
3
+ metadata.gz: e747e334297726a887a07b078e0419bc232dd00d90823007bc8fcc96bb8b6fce
4
+ data.tar.gz: 87f53111a3ca2f2d5e5e126f780c1e756f98c415d1a10c66bad8595fc9049940
5
5
  SHA512:
6
- metadata.gz: cca8fc5c8b38722ab95fecfeea576104c180352a9af854171858702d23a240368bef870b831c463601f06f6f04d76671aa8a5254edfc37880b3acef1aced476a
7
- data.tar.gz: 29f289c88b14fcbe64091256d61afaa1752a4f405a5a3f88e55b966c7fbc50076c6db6a5e88bbe4f1e4840f3a424c88e37daf167acdca64b1872d81f8a2dcc01
6
+ metadata.gz: c384201adbfeb713cd67d81f3dda4642d0e9efab8b3026cf6fe88f6de869728890ca60e5f9591c12c9d6b034d9c5d9f74aae77a796a327674c129f806466b1d2
7
+ data.tar.gz: 1e8b73525875288901086e755ce87cfe3f2d257ddfe234f55d6eca0b680b0841c8205e05d77f91550ca978618d79c8bf3ad9ce41740b87c92585ea306726ea36
data/README.md CHANGED
@@ -13,7 +13,7 @@
13
13
  <td width="50%" valign="top">
14
14
  <strong>Key Features</strong><br>
15
15
 
16
- - <strong>18 Coordinate Systems</strong> - LLA, ECEF, UTM, ENU, NED, MGRS, USNG, Web Mercator, UPS, State Plane, BNG, GH36, GH, HAM, OLC, GEOREF, GARS, H3<br>
16
+ - <strong>19 Coordinate Systems</strong> - LLA, ECEF, UTM, ENU, NED, MGRS, USNG, Web Mercator, UPS, State Plane, BNG, GH36, GH, HAM, OLC, GEOREF, GARS, H3, S2<br>
17
17
  - <strong>Full Bidirectional Conversions</strong> - Every system converts to and from every other system<br>
18
18
  - <strong>Distance Calculations</strong> - Vincenty great-circle and straight-line with unit tracking<br>
19
19
  - <strong>Bearing Calculations</strong> - Forward azimuth, back azimuth, compass directions, elevation angles<br>
@@ -36,7 +36,7 @@
36
36
  </tr>
37
37
  </table>
38
38
 
39
- <p>Geodetic enables precise conversion between geodetic coordinate systems in Ruby. All 18 coordinate systems support complete bidirectional conversions with high precision. Review the <a href="https://madbomber.github.io/geodetic/">full documentation website</a> and explore the <a href="examples/">runnable examples</a>.</p>
39
+ <p>Geodetic enables precise conversion between geodetic coordinate systems in Ruby. All 19 coordinate systems support complete bidirectional conversions with high precision. Review the <a href="https://madbomber.github.io/geodetic/">full documentation website</a> and explore the <a href="examples/">runnable examples</a>.</p>
40
40
 
41
41
  ## Installation
42
42
 
@@ -86,6 +86,24 @@ Set `GEODETIC_GEOS_DISABLE=1` to force pure Ruby for all operations, even when G
86
86
  Geodetic::Geos.available? # => true when libgeos_c is found and not disabled
87
87
  ```
88
88
 
89
+ ### Optional: S2 Spherical Geometry Index
90
+
91
+ The S2 coordinate system requires Google's [S2 Geometry library](https://github.com/google/s2geometry) installed on your system. S2 projects a cube onto the sphere and subdivides it into quadrilateral cells via a Hilbert space-filling curve, providing very low distortion (~0.56%) across the entire Earth surface. Cell IDs are 64-bit integers that enable efficient spatial database queries on standard B-tree indexes.
92
+
93
+ ```bash
94
+ # macOS
95
+ brew install s2geometry
96
+
97
+ # Linux (build from source)
98
+ # See https://github.com/google/s2geometry
99
+ ```
100
+
101
+ You can set the `LIBS2_PATH` environment variable to point to a custom `libs2` location. See [S2 documentation](docs/coordinate-systems/s2.md) for detailed API reference and [example 15](examples/15_s2_geometry.rb) for a comprehensive demo.
102
+
103
+ ```ruby
104
+ Geodetic::Coordinate::S2.available? # => true when libs2 is found
105
+ ```
106
+
89
107
  ## Usage
90
108
 
91
109
  ### Basic Coordinate Creation
@@ -132,7 +150,7 @@ Geodetic::Coordinate.systems
132
150
  # Get short names
133
151
  Geodetic::Coordinate.systems.map { |c| c.name.split('::').last }
134
152
  # => ["LLA", "ECEF", "UTM", "ENU", "NED", "MGRS", "USNG", "WebMercator",
135
- # "UPS", "StatePlane", "BNG", "GH36", "GH", "HAM", "OLC", "GEOREF", "GARS", "H3"]
153
+ # "UPS", "StatePlane", "BNG", "GH36", "GH", "HAM", "OLC", "GEOREF", "GARS", "H3", "S2"]
136
154
  ```
137
155
 
138
156
  ### Coordinate Conversions
@@ -546,6 +564,51 @@ olc.precision # => 10
546
564
  olc.precision_in_meters # => { lat: 13.9, lng: 13.9 }
547
565
  ```
548
566
 
567
+ ### S2 Spherical Geometry Index
568
+
569
+ Google's hierarchical spatial index using a cube-on-sphere projection with Hilbert curve ordering. Cell IDs are 64-bit integers displayed as hex tokens. See [S2 documentation](docs/coordinate-systems/s2.md) for the full API.
570
+
571
+ ```ruby
572
+ # From a token string or integer
573
+ s2 = Coordinate::S2.new("54906ab14")
574
+ s2 = Coordinate::S2.new(6093487605347778560)
575
+
576
+ # From any coordinate
577
+ s2 = Coordinate::S2.new(lla)
578
+ s2 = lla.to_s2(precision: 20) # level 20
579
+
580
+ # Decode back to LLA
581
+ lla = s2.to_lla
582
+
583
+ # Properties
584
+ s2.level # => 15
585
+ s2.face # => 2 (cube face 0-5)
586
+ s2.cell_id # => 6093487605347778560 (for database storage)
587
+ s2.to_s # => "54906ab14" (token for display)
588
+
589
+ # Hierarchy
590
+ s2.parent(10) # => S2 at level 10
591
+ s2.children # => [S2, S2, S2, S2] at level 16
592
+
593
+ # Neighbors (4 edge-adjacent cells)
594
+ s2.neighbors # => [S2, S2, S2, S2]
595
+
596
+ # Containment
597
+ parent.contains?(child) # => true
598
+ parent.intersects?(child) # => true
599
+
600
+ # Cell area (exact spherical surface area)
601
+ s2.cell_area # => 77544.2 (square meters)
602
+
603
+ # Cell boundary as polygon
604
+ polygon = s2.to_area # => Areas::Polygon with 4 vertices
605
+ polygon.includes?(s2.to_lla) # => true
606
+
607
+ # Database range scans (spatial queries on B-tree indexes)
608
+ s2.range_min # => start of cell ID range
609
+ s2.range_max # => end of cell ID range
610
+ ```
611
+
549
612
  ### Geographic Areas
550
613
 
551
614
  ```ruby
@@ -896,6 +959,8 @@ The [`examples/`](examples/) directory contains runnable demo scripts showing pr
896
959
  | [`11_wkb_serialization.rb`](examples/11_wkb_serialization.rb) | WKB serialization: `to_wkb`/`to_wkb_hex` on all geometry types, EWKB/SRID, Z-dimension, parsing, roundtrip, and binary/hex file I/O |
897
960
  | [`12_geos_benchmark.rb`](examples/12_geos_benchmark.rb) | GEOS performance benchmark: polygon validation, point-in-polygon, path intersection, PreparedGeometry batch containment, and GEOS-only boolean operations |
898
961
  | [`13_geos_operations.rb`](examples/13_geos_operations.rb) | GEOS-only operations: boolean overlay (intersection, difference, symmetric difference, union), buffering, convex hull, simplification, validity checking, geometry repair, planar measurements, nearest points, PreparedGeometry, and operation chaining |
962
+ | [`14_geos_map_rendering.rb`](examples/14_geos_map_rendering.rb) | GEOS map rendering: visualizes 8 GEOS operation categories on a single raster map with distinct colors and an embedded legend |
963
+ | [`15_s2_geometry.rb`](examples/15_s2_geometry.rb) | S2 Geometry: construction, round-trip conversion, cell hierarchy (parent/children), edge neighbors, containment/intersection, cell area calculations, cell polygons, database range scans, cross-hash conversions, distance/bearing, arithmetic, serialization (WKT/WKB/GeoJSON), six cube faces, and performance benchmarks |
899
964
 
900
965
  Run any example with:
901
966
 
@@ -1,6 +1,6 @@
1
1
  # Coordinate Systems Overview
2
2
 
3
- The Geodetic gem supports 18 coordinate systems organized into six categories. All coordinate classes live under `Geodetic::Coordinate`.
3
+ The Geodetic gem supports 19 coordinate systems organized into six categories. All coordinate classes live under `Geodetic::Coordinate`.
4
4
 
5
5
  ## Global Systems
6
6
 
@@ -47,6 +47,7 @@ The Geodetic gem supports 18 coordinate systems organized into six categories. A
47
47
  | [**GEOREF**](georef.md) | `Geodetic::Coordinate::GEOREF` | World Geographic Reference System. A geocode system used in aviation and military applications that encodes positions using letter tiles (15° grid), letter degree subdivisions, and numeric minute pairs. Uses a 24-letter alphabet (A-Z excluding I and O). Supports variable precision from 15° tiles (2 chars) down to 0.01-minute resolution (12 chars). Default precision is 8 characters (1-minute resolution). |
48
48
  | [**GARS**](gars.md) | `Geodetic::Coordinate::GARS` | Global Area Reference System. An NGA standard that divides the world into 30-minute cells identified by a 3-digit longitude band (001-720) and 2-letter latitude band. Cells are subdivided into 15-minute quadrants (1-4) and 5-minute keypads (1-9, telephone layout). Variable precision: 5 chars (30'), 6 chars (15'), 7 chars (5'). Default precision is 7 characters. |
49
49
  | [**H3**](h3.md) | `Geodetic::Coordinate::H3` | H3 Hexagonal Hierarchical Index. Uber's spatial indexing system that divides the globe into hexagonal cells (and 12 pentagons) at 16 resolution levels (0-15). Each cell is a 64-bit integer displayed as a hex string. Unlike the rectangular spatial hashes, `to_area` returns an `Areas::Polygon` with 6 vertices (5 for pentagons) and `neighbors` returns an Array of 6 cells. **Requires `libh3` installed** (`brew install h3` on macOS). |
50
+ | [**S2**](s2.md) | `Geodetic::Coordinate::S2` | S2 Spherical Geometry Index. Google's hierarchical spatial indexing system that projects a cube onto the unit sphere, subdividing into quadrilateral cells via a Hilbert space-filling curve. 31 levels (0-30) with 64-bit cell IDs displayed as hex tokens. Cells are geodesic quadrilaterals with very low distortion (~0.56%). `to_area` returns an `Areas::Polygon` with 4 vertices. `neighbors` returns an Array of 4 edge cells. Cell IDs enable efficient database range scans on standard B-tree indexes. **Requires `libs2` installed** (`brew install s2geometry` on macOS). |
50
51
 
51
52
  ## Regional Systems
52
53
 
@@ -61,26 +62,27 @@ The Geodetic gem supports 18 coordinate systems organized into six categories. A
61
62
 
62
63
  Every coordinate system can convert to every other coordinate system. The table below confirms full interoperability:
63
64
 
64
- | From \ To | LLA | ECEF | UTM | ENU | NED | MGRS | USNG | WebMercator | UPS | StatePlane | BNG | GH36 | GH | HAM | OLC | GEOREF | GARS | H3 |
65
- |-----------|-----|------|-----|-----|-----|------|------|-------------|-----|------------|-----|------|----|----|-----|--------|------|-----|
66
- | **LLA** | -- | Y | Y | Y | Y | Y | Y | Y | Y | Y | Y | Y | Y | Y | Y | Y | Y | Y |
67
- | **ECEF** | Y | -- | Y | Y | Y | Y | Y | Y | Y | Y | Y | Y | Y | Y | Y | Y | Y | Y |
68
- | **UTM** | Y | Y | -- | Y | Y | Y | Y | Y | Y | Y | Y | Y | Y | Y | Y | Y | Y | Y |
69
- | **ENU** | Y | Y | Y | -- | Y | Y | Y | Y | Y | Y | Y | Y | Y | Y | Y | Y | Y | Y |
70
- | **NED** | Y | Y | Y | Y | -- | Y | Y | Y | Y | Y | Y | Y | Y | Y | Y | Y | Y | Y |
71
- | **MGRS** | Y | Y | Y | Y | Y | -- | Y | Y | Y | Y | Y | Y | Y | Y | Y | Y | Y | Y |
72
- | **USNG** | Y | Y | Y | Y | Y | Y | -- | Y | Y | Y | Y | Y | Y | Y | Y | Y | Y | Y |
73
- | **WebMercator**| Y | Y | Y | Y | Y | Y | Y | -- | Y | Y | Y | Y | Y | Y | Y | Y | Y | Y |
74
- | **UPS** | Y | Y | Y | Y | Y | Y | Y | Y | -- | Y | Y | Y | Y | Y | Y | Y | Y | Y |
75
- | **StatePlane** | Y | Y | Y | Y | Y | Y | Y | Y | Y | -- | Y | Y | Y | Y | Y | Y | Y | Y |
76
- | **BNG** | Y | Y | Y | Y | Y | Y | Y | Y | Y | Y | -- | Y | Y | Y | Y | Y | Y | Y |
77
- | **GH36** | Y | Y | Y | Y | Y | Y | Y | Y | Y | Y | Y | -- | Y | Y | Y | Y | Y | Y |
78
- | **GH** | Y | Y | Y | Y | Y | Y | Y | Y | Y | Y | Y | Y | -- | Y | Y | Y | Y | Y |
79
- | **HAM** | Y | Y | Y | Y | Y | Y | Y | Y | Y | Y | Y | Y | Y | -- | Y | Y | Y | Y |
80
- | **OLC** | Y | Y | Y | Y | Y | Y | Y | Y | Y | Y | Y | Y | Y | Y | -- | Y | Y | Y |
81
- | **GEOREF** | Y | Y | Y | Y | Y | Y | Y | Y | Y | Y | Y | Y | Y | Y | Y | -- | Y | Y |
82
- | **GARS** | Y | Y | Y | Y | Y | Y | Y | Y | Y | Y | Y | Y | Y | Y | Y | Y | -- | Y |
83
- | **H3** | Y | Y | Y | Y | Y | Y | Y | Y | Y | Y | Y | Y | Y | Y | Y | Y | Y | -- |
65
+ | From \ To | LLA | ECEF | UTM | ENU | NED | MGRS | USNG | WebMercator | UPS | StatePlane | BNG | GH36 | GH | HAM | OLC | GEOREF | GARS | H3 | S2 |
66
+ |-----------|-----|------|-----|-----|-----|------|------|-------------|-----|------------|-----|------|----|----|-----|--------|------|-----|-----|
67
+ | **LLA** | -- | Y | Y | Y | Y | Y | Y | Y | Y | Y | Y | Y | Y | Y | Y | Y | Y | Y | Y |
68
+ | **ECEF** | Y | -- | Y | Y | Y | Y | Y | Y | Y | Y | Y | Y | Y | Y | Y | Y | Y | Y | Y |
69
+ | **UTM** | Y | Y | -- | Y | Y | Y | Y | Y | Y | Y | Y | Y | Y | Y | Y | Y | Y | Y | Y |
70
+ | **ENU** | Y | Y | Y | -- | Y | Y | Y | Y | Y | Y | Y | Y | Y | Y | Y | Y | Y | Y | Y |
71
+ | **NED** | Y | Y | Y | Y | -- | Y | Y | Y | Y | Y | Y | Y | Y | Y | Y | Y | Y | Y | Y |
72
+ | **MGRS** | Y | Y | Y | Y | Y | -- | Y | Y | Y | Y | Y | Y | Y | Y | Y | Y | Y | Y | Y |
73
+ | **USNG** | Y | Y | Y | Y | Y | Y | -- | Y | Y | Y | Y | Y | Y | Y | Y | Y | Y | Y | Y |
74
+ | **WebMercator**| Y | Y | Y | Y | Y | Y | Y | -- | Y | Y | Y | Y | Y | Y | Y | Y | Y | Y | Y |
75
+ | **UPS** | Y | Y | Y | Y | Y | Y | Y | Y | -- | Y | Y | Y | Y | Y | Y | Y | Y | Y | Y |
76
+ | **StatePlane** | Y | Y | Y | Y | Y | Y | Y | Y | Y | -- | Y | Y | Y | Y | Y | Y | Y | Y | Y |
77
+ | **BNG** | Y | Y | Y | Y | Y | Y | Y | Y | Y | Y | -- | Y | Y | Y | Y | Y | Y | Y | Y |
78
+ | **GH36** | Y | Y | Y | Y | Y | Y | Y | Y | Y | Y | Y | -- | Y | Y | Y | Y | Y | Y | Y |
79
+ | **GH** | Y | Y | Y | Y | Y | Y | Y | Y | Y | Y | Y | Y | -- | Y | Y | Y | Y | Y | Y |
80
+ | **HAM** | Y | Y | Y | Y | Y | Y | Y | Y | Y | Y | Y | Y | Y | -- | Y | Y | Y | Y | Y |
81
+ | **OLC** | Y | Y | Y | Y | Y | Y | Y | Y | Y | Y | Y | Y | Y | Y | -- | Y | Y | Y | Y |
82
+ | **GEOREF** | Y | Y | Y | Y | Y | Y | Y | Y | Y | Y | Y | Y | Y | Y | Y | -- | Y | Y | Y |
83
+ | **GARS** | Y | Y | Y | Y | Y | Y | Y | Y | Y | Y | Y | Y | Y | Y | Y | Y | -- | Y | Y |
84
+ | **H3** | Y | Y | Y | Y | Y | Y | Y | Y | Y | Y | Y | Y | Y | Y | Y | Y | Y | -- | Y |
85
+ | **S2** | Y | Y | Y | Y | Y | Y | Y | Y | Y | Y | Y | Y | Y | Y | Y | Y | Y | Y | -- |
84
86
 
85
87
  ## Universal Distance and Bearing Calculations
86
88
 
@@ -100,6 +102,6 @@ Conversions typically route through **LLA** or **ECEF** as intermediate steps:
100
102
  - **ECEF** is the intermediate for local tangent plane systems (ENU, NED), since the rotation from global Cartesian to local frames is straightforward in ECEF.
101
103
  - **ENU and NED** convert between each other directly by reordering axes and inverting the vertical component.
102
104
  - **MGRS and USNG** route through UTM, which in turn routes through LLA.
103
- - **WebMercator, UPS, BNG, StatePlane, GH36, GH, HAM, OLC, GEOREF, GARS, and H3** all convert through LLA.
105
+ - **WebMercator, UPS, BNG, StatePlane, GH36, GH, HAM, OLC, GEOREF, GARS, H3, and S2** all convert through LLA.
104
106
 
105
107
  For example, converting from BNG to NED follows the chain: `BNG -> LLA -> ECEF -> ENU -> NED`. The gem handles this automatically when you call a conversion method.
@@ -0,0 +1,451 @@
1
+ # Geodetic::Coordinate::S2
2
+
3
+ ## S2 Spherical Geometry Index
4
+
5
+ S2 is Google's hierarchical geospatial indexing system. It projects a cube onto the unit sphere, then recursively subdivides each of the six cube faces into quadrilateral cells using a Hilbert space-filling curve. Each cell is identified by a 64-bit integer called a **cell ID**, typically displayed as a **token** — a hex string with trailing zeros stripped.
6
+
7
+ The name comes from the mathematical notation S² for the unit sphere.
8
+
9
+ **S2 requires the `libs2` C++ library** installed on your system. Without it, all other coordinate systems work normally; S2 operations raise a clear error message with installation instructions.
10
+
11
+ ### Prerequisites
12
+
13
+ ```bash
14
+ # macOS (Homebrew)
15
+ brew install s2geometry
16
+
17
+ # Linux (build from source)
18
+ git clone https://github.com/google/s2geometry.git
19
+ cd s2geometry
20
+ cmake -B build -DCMAKE_INSTALL_PREFIX=/usr/local
21
+ cmake --build build
22
+ sudo cmake --install build
23
+ ```
24
+
25
+ You can also set the `LIBS2_PATH` environment variable to specify a custom library path:
26
+
27
+ ```bash
28
+ export LIBS2_PATH=/path/to/libs2.dylib
29
+ ```
30
+
31
+ ### Key Differences from Other Spatial Hashes
32
+
33
+ | Feature | GH/OLC/GARS/GEOREF/HAM | H3 | S2 |
34
+ |---------|------------------------|-----|-----|
35
+ | Cell shape | Rectangle | Hexagon (6 vertices) | Quadrilateral (4 vertices) |
36
+ | `to_area` returns | `Areas::BoundingBox` | `Areas::Polygon` (6 verts) | `Areas::Polygon` (4 verts) |
37
+ | `neighbors` returns | Hash with 8 cardinal keys | Array of 6 cells | Array of 4 edge cells |
38
+ | Code format | String | 64-bit integer (hex) | 64-bit integer (hex token) |
39
+ | Hierarchy | String prefix | Parent/child by resolution | Parent/child by level (quadtree) |
40
+ | Levels | String length | 0-15 | 0-30 |
41
+ | Projection | Rectangular lat/lng | Icosahedron | Cube-on-sphere |
42
+ | Distortion | High at poles | Low (~1.2x) | Very low (~0.56%) |
43
+ | Spatial ordering | Z-order (GH) / none | None guaranteed | Hilbert curve (locality) |
44
+ | Dependency | None (pure Ruby) | `libh3` (C) | `libs2` (C++) |
45
+
46
+ S2 is a **2D coordinate system** (no altitude). Conversions to/from other systems go through LLA as the intermediary. Each token represents a quadrilateral cell; the coordinate's point value is the cell's centroid.
47
+
48
+ ### Cell ID vs Token
49
+
50
+ A cell ID and a token represent the same cell in two formats:
51
+
52
+ - **cell_id** — the raw 64-bit unsigned integer. Use this for database storage and range queries. Standard B-tree indexes on a `BIGINT` column give you spatial queries for free.
53
+
54
+ - **token** — the cell_id printed as hex with trailing zeros stripped. Use this for display, logging, and human-readable interchange. Shorter tokens represent coarser (larger) cells.
55
+
56
+ ```ruby
57
+ cell = S2.new(seattle, precision: 15)
58
+ cell.cell_id # => 6093487605347778560
59
+ cell.to_s # => "54906ab14"
60
+
61
+ # Equivalence: token is just compact hex of cell_id
62
+ "54906ab14".ljust(16, '0').to_i(16) # => 6093487605347778560
63
+ ```
64
+
65
+ Tokens naturally reveal the hierarchy — a parent's token is always a prefix of its descendants:
66
+
67
+ ```
68
+ Level 0: "5" (face 2)
69
+ Level 5: "5494"
70
+ Level 10: "54906b"
71
+ Level 15: "54906ab14"
72
+ Level 20: "54906ab12f1"
73
+ Level 30: "54906ab12f10f899"
74
+ ```
75
+
76
+ For **database applications**, store the cell_id as a `BIGINT`. Spatial containment queries become range scans:
77
+
78
+ ```sql
79
+ -- Find all points inside a level-12 cell
80
+ SELECT * FROM locations
81
+ WHERE s2_cell_id BETWEEN :range_min AND :range_max
82
+ ```
83
+
84
+ The Hilbert curve ordering guarantees that spatially nearby cells have numerically nearby cell_ids, so these range scans are efficient on standard B-tree indexes — no spatial index extension required.
85
+
86
+ ## Constructor
87
+
88
+ ```ruby
89
+ # From a token string
90
+ coord = Geodetic::Coordinate::S2.new("54906ab14")
91
+
92
+ # From a token with 0x prefix
93
+ coord = Geodetic::Coordinate::S2.new("0x54906ab14")
94
+
95
+ # From a 64-bit cell ID integer
96
+ coord = Geodetic::Coordinate::S2.new(6093487605347778560)
97
+
98
+ # From any coordinate (converts via LLA)
99
+ coord = Geodetic::Coordinate::S2.new(lla_coord)
100
+ coord = Geodetic::Coordinate::S2.new(utm_coord, precision: 20)
101
+ ```
102
+
103
+ | Parameter | Type | Default | Description |
104
+ |-------------|-------------------------|---------|------------------------------------------|
105
+ | `source` | String, Integer, or Coord | -- | An S2 token, cell ID integer, or coordinate |
106
+ | `precision` | Integer | 15 | S2 cell level (0-30) |
107
+
108
+ Raises `ArgumentError` if the source string is empty, contains invalid hex characters, or does not produce a valid S2 cell ID. String input is case-insensitive (normalized to lowercase). The `0x` prefix is stripped automatically.
109
+
110
+ ## Attributes
111
+
112
+ | Attribute | Type | Access | Description |
113
+ |-----------|---------|-----------|-----------------------------------|
114
+ | `code` | String | read-only | The token string representation |
115
+ | `cell_id` | Integer | read-only | The 64-bit S2 cell ID |
116
+
117
+ S2 is **immutable** — there are no setter methods.
118
+
119
+ ## Levels
120
+
121
+ S2 uses "level" (0-30) for its hierarchy. Higher level means smaller cells. Each level subdivides cells by 4 (quadtree), so area decreases by roughly 4x per level.
122
+
123
+ | Level | Approximate Cell Area | Approximate Edge Length |
124
+ |-------|----------------------|------------------------|
125
+ | 0 | 85,201,316 km² | ~9,200 km (face cell) |
126
+ | 1 | 21,300,329 km² | ~4,600 km |
127
+ | 5 | 79,002 km² | ~281 km |
128
+ | 10 | 79.4 km² | ~8.9 km |
129
+ | 12 | 5.0 km² | ~2.2 km |
130
+ | 15 | 77,544 m² (default) | ~278 m |
131
+ | 18 | 1,212 m² | ~35 m |
132
+ | 20 | 75.7 m² | ~8.7 m |
133
+ | 24 | 0.30 m² (2,958 cm²) | ~54 cm |
134
+ | 28 | 11.55 cm² | ~3.4 cm |
135
+ | 30 | 0.72 cm² | ~0.85 cm |
136
+
137
+ ```ruby
138
+ coord.level # => 15 (alias: coord.precision)
139
+ coord.cell_area # => 77544.2 (square meters)
140
+ coord.precision_in_meters # => { lat: ~278, lng: ~278, area_m2: ~77544 }
141
+ S2.average_cell_area(15) # => 79349.9 (average across all level-15 cells)
142
+ ```
143
+
144
+ ## The Six Cube Faces
145
+
146
+ S2 projects a cube onto the sphere, giving 6 face cells at level 0:
147
+
148
+ | Face | Direction | Center | Token |
149
+ |------|-------------|----------------|-------|
150
+ | 0 | +X (front) | (0°, 0°) | `1` |
151
+ | 1 | +Y (right) | (0°, 90°E) | `3` |
152
+ | 2 | +Z (top) | (90°N, 0°) | `5` |
153
+ | 3 | -X (back) | (0°, 180°) | `7` |
154
+ | 4 | -Y (left) | (0°, 90°W) | `9` |
155
+ | 5 | -Z (bottom) | (90°S, 0°) | `b` |
156
+
157
+ The cube-on-sphere projection limits distortion to approximately 0.56% across the entire Earth surface — far less than rectangular projections which distort heavily near the poles.
158
+
159
+ ```ruby
160
+ coord.face # => 2 (Seattle is on face 2, the +Z/north pole face)
161
+ coord.face_cell? # => false (only level-0 cells are face cells)
162
+ ```
163
+
164
+ ## Checking Availability
165
+
166
+ ```ruby
167
+ Geodetic::Coordinate::S2.available? # => true if libs2 is found
168
+ ```
169
+
170
+ ## Conversions
171
+
172
+ All conversions chain through LLA. The datum parameter defaults to `Geodetic::WGS84`.
173
+
174
+ ### Instance Methods
175
+
176
+ ```ruby
177
+ coord.to_lla # => LLA (centroid of the cell)
178
+ coord.to_ecef
179
+ coord.to_utm
180
+ coord.to_enu(reference_lla)
181
+ coord.to_ned(reference_lla)
182
+ coord.to_mgrs
183
+ coord.to_usng
184
+ coord.to_web_mercator
185
+ coord.to_ups
186
+ coord.to_state_plane(zone_code)
187
+ coord.to_bng
188
+ coord.to_gh36
189
+ coord.to_gh
190
+ coord.to_ham
191
+ coord.to_olc
192
+ coord.to_georef
193
+ coord.to_gars
194
+ coord.to_h3 # (requires libh3)
195
+ ```
196
+
197
+ ### Class Methods
198
+
199
+ ```ruby
200
+ S2.from_lla(lla_coord)
201
+ S2.from_ecef(ecef_coord)
202
+ S2.from_utm(utm_coord)
203
+ S2.from_web_mercator(wm_coord)
204
+ S2.from_gh(gh_coord)
205
+ S2.from_h3(h3_coord)
206
+ # ... and all other coordinate systems
207
+ ```
208
+
209
+ ### LLA Convenience Methods
210
+
211
+ ```ruby
212
+ lla = Geodetic::Coordinate::LLA.new(lat: 47.6062, lng: -122.3321)
213
+ s2 = lla.to_s2 # default level 15
214
+ s2 = lla.to_s2(precision: 20) # level 20
215
+
216
+ lla = Geodetic::Coordinate::LLA.from_s2(s2)
217
+ ```
218
+
219
+ ## Serialization
220
+
221
+ ### `to_s(format = nil)`
222
+
223
+ Returns the token string. Pass `:integer` to get the 64-bit cell ID.
224
+
225
+ ```ruby
226
+ coord = S2.new("54906ab14")
227
+ coord.to_s # => "54906ab14"
228
+ coord.to_s(:integer) # => 6093487605347778560
229
+ coord.cell_id # => 6093487605347778560
230
+ ```
231
+
232
+ ### `to_a`
233
+
234
+ Returns `[lat, lng]` of the cell centroid.
235
+
236
+ ```ruby
237
+ coord.to_a # => [47.605..., -122.334...]
238
+ ```
239
+
240
+ ### `from_string` / `from_array`
241
+
242
+ ```ruby
243
+ S2.from_string("54906ab14") # from token
244
+ S2.from_array([47.6062, -122.3321]) # from [lat, lng]
245
+ ```
246
+
247
+ ### WKT, WKB, GeoJSON
248
+
249
+ S2 coordinates support all standard serialization formats:
250
+
251
+ ```ruby
252
+ coord.to_wkt # => "POINT(-122.334371 47.605024)"
253
+ coord.to_wkt(srid: 4326) # => "SRID=4326;POINT(-122.334371 47.605024)"
254
+ coord.to_wkb_hex # => "010100000014dbfb5466955ec0..."
255
+ coord.to_geojson # => {"type" => "Point", "coordinates" => [...]}
256
+
257
+ # Cell polygon serialization
258
+ coord.to_area.to_wkt # => "POLYGON((-122.33392 47.606536, ...))"
259
+ coord.to_area.to_geojson # => {"type" => "Polygon", ...}
260
+ ```
261
+
262
+ ## Cell Hierarchy
263
+
264
+ S2 cells form a strict quadtree: each cell has exactly 4 children and 1 parent (except level-0 face cells which have no parent).
265
+
266
+ ### Parent
267
+
268
+ ```ruby
269
+ coord = S2.new("54906ab14") # level 15
270
+ parent = coord.parent # level 14 (one level up)
271
+ parent = coord.parent(10) # level 10 (5 levels up)
272
+ parent = coord.parent(0) # level 0 (face cell)
273
+ ```
274
+
275
+ Raises `ArgumentError` if the target level is not coarser (lower number) than the current level.
276
+
277
+ ### Children
278
+
279
+ ```ruby
280
+ coord = S2.new("54906ab14") # level 15
281
+ children = coord.children # => [S2, S2, S2, S2] at level 16
282
+ children.length # => 4
283
+ ```
284
+
285
+ Raises `ArgumentError` on leaf cells (level 30).
286
+
287
+ ### Ancestry Traversal
288
+
289
+ ```ruby
290
+ leaf = S2.new(seattle, precision: 30)
291
+ [30, 20, 10, 0].each do |lvl|
292
+ ancestor = lvl == 30 ? leaf : leaf.parent(lvl)
293
+ puts "Level #{lvl}: #{ancestor.to_s}"
294
+ end
295
+ ```
296
+
297
+ ## Neighbors
298
+
299
+ Returns the 4 edge-adjacent cells as an Array. Unlike rectangular hashes (which return 8 directional neighbors), S2 cells are quadrilaterals with exactly 4 edges.
300
+
301
+ ```ruby
302
+ coord = S2.new("54906ab14")
303
+ neighbors = coord.neighbors
304
+ # => [S2, S2, S2, S2]
305
+
306
+ neighbors.length # => 4
307
+ neighbors.first.level # => 15 (same level as source)
308
+ ```
309
+
310
+ ## Containment and Intersection
311
+
312
+ S2 cells have strict hierarchical containment — a cell contains exactly its descendants and intersects exactly its ancestors and descendants.
313
+
314
+ ```ruby
315
+ coarse = S2.new(seattle, precision: 10)
316
+ fine = S2.new(seattle, precision: 20)
317
+
318
+ coarse.contains?(fine) # => true
319
+ fine.contains?(coarse) # => false
320
+ coarse.intersects?(fine) # => true
321
+ fine.intersects?(coarse) # => true
322
+ ```
323
+
324
+ ## Database Range Scans
325
+
326
+ Every S2 cell defines a contiguous range of cell IDs that covers all its descendants. This enables spatial queries as simple integer range scans.
327
+
328
+ ```ruby
329
+ cell = S2.new(seattle, precision: 12)
330
+ cell.range_min # => 6093487531259592705
331
+ cell.range_max # => 6093487668698546175
332
+ cell.cell_id # => 6093487599767896064 (between min and max)
333
+ ```
334
+
335
+ Any descendant cell's ID falls within `[range_min, range_max]`:
336
+
337
+ ```ruby
338
+ child = S2.new(seattle, precision: 20)
339
+ child.cell_id >= cell.range_min # => true
340
+ child.cell_id <= cell.range_max # => true
341
+ ```
342
+
343
+ ## Cell Area
344
+
345
+ S2 provides exact cell area calculations using the C++ library's spherical geometry.
346
+
347
+ ```ruby
348
+ coord.cell_area # => 77544.2 (square meters for level 15)
349
+ S2.average_cell_area(15) # => 79349.9 (global average for level 15)
350
+ coord.precision_in_meters # => { lat: 278.5, lng: 278.5, area_m2: 77544.2 }
351
+ ```
352
+
353
+ Cell areas are computed as exact spherical surface area (steradians) multiplied by R², giving results in square meters.
354
+
355
+ ## Cell Polygon (to_area)
356
+
357
+ The `to_area` method returns the quadrilateral cell boundary as an `Areas::Polygon` with 4 vertices.
358
+
359
+ ```ruby
360
+ area = coord.to_area
361
+ # => Geodetic::Areas::Polygon
362
+
363
+ area.includes?(coord.to_lla) # => true (centroid is inside the cell)
364
+ area.boundary.length # => 5 (4 vertices + closing point)
365
+ ```
366
+
367
+ S2 cell boundaries are geodesics (great circle arcs), not straight lines in lat/lng space. The polygon approximation uses the 4 corner vertices connected by straight edges.
368
+
369
+ ## Leaf and Face Cells
370
+
371
+ ```ruby
372
+ coord.leaf? # => true if level == 30 (smallest possible cell, ~0.7 cm²)
373
+ coord.face_cell? # => true if level == 0 (one of 6 cube faces, ~85M km²)
374
+ ```
375
+
376
+ ## Equality
377
+
378
+ Two S2 instances are equal if their token strings match exactly.
379
+
380
+ ```ruby
381
+ S2.new("54906ab14") == S2.new("54906ab14") # => true
382
+ S2.new("54906ab14") == S2.new(6093487605347778560) # => true
383
+ S2.new("54906ab14") == S2.new("54906ab1c") # => false
384
+ ```
385
+
386
+ ## `valid?`
387
+
388
+ Returns `true` if the cell ID has valid structure (correct face bits, properly positioned sentinel bit).
389
+
390
+ ```ruby
391
+ coord.valid? # => true
392
+ ```
393
+
394
+ ## Universal Distance and Bearing Methods
395
+
396
+ S2 supports all universal distance and bearing methods via the `DistanceMethods` and `BearingMethods` mixins:
397
+
398
+ ```ruby
399
+ seattle = S2.new(Coordinate::LLA.new(lat: 47.6062, lng: -122.3321), precision: 15)
400
+ nyc = S2.new(Coordinate::LLA.new(lat: 40.7128, lng: -74.0060), precision: 15)
401
+
402
+ seattle.distance_to(nyc) # => Distance (~3,876 km)
403
+ seattle.straight_line_distance_to(nyc) # => Distance
404
+ seattle.bearing_to(nyc) # => Bearing (~83°, roughly east)
405
+ seattle.elevation_to(nyc) # => Float (degrees)
406
+ ```
407
+
408
+ ## Geodetic Arithmetic
409
+
410
+ S2 cells support the standard arithmetic operators:
411
+
412
+ ```ruby
413
+ s2 = S2.new(seattle, precision: 15)
414
+ v = Geodetic::Vector.new(distance: 10_000, bearing: 90)
415
+
416
+ s2 * v # => LLA (translated cell center)
417
+ s2 + v # => Segment (from center to destination)
418
+ s2 + distance # => Circle (centered on cell center)
419
+ s2 + other_s2 # => Segment (between cell centers)
420
+ ```
421
+
422
+ ## Well-Known S2 Tokens
423
+
424
+ | Location | Token (level 30) | Token (level 15) | Face |
425
+ |----------|-----------------|-------------------|------|
426
+ | Seattle | `54906ab12f10f899` | `54906ab14` | 2 |
427
+ | New York | `89c25a220cf80969` | `89c25a224` | 4 |
428
+ | Tokyo | `6018f25555544b7f` | `6018f254c` | 3 |
429
+ | London | `487604ce36748fa9` | `487604d04` | 2 |
430
+ | Sydney | `6b12ae3ff6290055` | `6b12ae3fc` | 3 |
431
+ | Null Island (0°, 0°) | `1000000000000001` | `1000000000000004` | 0 |
432
+
433
+ ## Implementation Notes
434
+
435
+ Geodetic uses Ruby's `fiddle` (part of the standard library) to call the S2 C++ library directly. Despite S2 being C++ (not C), the key functions are callable through their mangled symbol names. No gem dependency beyond `fiddle` is required. The S2 C++ library must be installed separately.
436
+
437
+ **Functions called via Fiddle:**
438
+ - `S2CellId::S2CellId(S2LatLng const&)` — encode lat/lng to cell ID
439
+ - `S2CellId::ToFaceIJOrientation()` — decode cell ID to face/IJ coordinates
440
+ - `S2CellId::GetEdgeNeighbors()` — 4 adjacent cells
441
+ - `S2Cell::ExactArea()` — spherical surface area in steradians
442
+ - `S2Cell::AverageArea(level)` — average area for a level
443
+
444
+ **Pure Ruby operations** (no FFI overhead):
445
+ - Cell level, face, parent, child — bit manipulation on the 64-bit cell ID
446
+ - Token encode/decode — hex formatting with trailing zero stripping
447
+ - Coordinate transforms — face/IJ → ST → UV → XYZ → lat/lng pipeline
448
+ - Cell vertex computation — 4 corners via the coordinate pipeline
449
+ - Containment/intersection — integer range comparisons
450
+ - Range min/max — bit arithmetic for database scan ranges
451
+ - Validity checks — sentinel bit position verification