geodetic 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 (60) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +28 -4
  3. data/README.md +19 -5
  4. data/docs/coordinate-systems/bng.md +5 -5
  5. data/docs/coordinate-systems/ecef.md +23 -23
  6. data/docs/coordinate-systems/enu.md +3 -3
  7. data/docs/coordinate-systems/gars.md +246 -0
  8. data/docs/coordinate-systems/georef.md +221 -0
  9. data/docs/coordinate-systems/gh.md +7 -7
  10. data/docs/coordinate-systems/gh36.md +6 -6
  11. data/docs/coordinate-systems/h3.md +312 -0
  12. data/docs/coordinate-systems/ham.md +6 -6
  13. data/docs/coordinate-systems/index.md +40 -34
  14. data/docs/coordinate-systems/lla.md +26 -26
  15. data/docs/coordinate-systems/mgrs.md +3 -3
  16. data/docs/coordinate-systems/ned.md +3 -3
  17. data/docs/coordinate-systems/olc.md +6 -6
  18. data/docs/coordinate-systems/state-plane.md +2 -2
  19. data/docs/coordinate-systems/ups.md +4 -4
  20. data/docs/coordinate-systems/usng.md +2 -2
  21. data/docs/coordinate-systems/utm.md +23 -23
  22. data/docs/coordinate-systems/web-mercator.md +7 -7
  23. data/docs/getting-started/installation.md +17 -17
  24. data/docs/getting-started/quick-start.md +8 -8
  25. data/docs/index.md +22 -19
  26. data/docs/reference/areas.md +15 -15
  27. data/docs/reference/conversions.md +31 -31
  28. data/docs/reference/geoid-height.md +5 -5
  29. data/docs/reference/serialization.md +44 -44
  30. data/examples/01_basic_conversions.rb +10 -10
  31. data/examples/02_all_coordinate_systems.rb +24 -24
  32. data/lib/geodetic/areas/circle.rb +1 -1
  33. data/lib/geodetic/areas/polygon.rb +2 -2
  34. data/lib/geodetic/areas/rectangle.rb +6 -6
  35. data/lib/geodetic/{coordinates → coordinate}/bng.rb +3 -37
  36. data/lib/geodetic/{coordinates → coordinate}/ecef.rb +3 -33
  37. data/lib/geodetic/{coordinates → coordinate}/enu.rb +30 -1
  38. data/lib/geodetic/coordinate/gars.rb +233 -0
  39. data/lib/geodetic/coordinate/georef.rb +204 -0
  40. data/lib/geodetic/coordinate/gh.rb +161 -0
  41. data/lib/geodetic/{coordinates → coordinate}/gh36.rb +28 -187
  42. data/lib/geodetic/coordinate/h3.rb +413 -0
  43. data/lib/geodetic/coordinate/ham.rb +226 -0
  44. data/lib/geodetic/{coordinates → coordinate}/lla.rb +31 -1
  45. data/lib/geodetic/{coordinates → coordinate}/mgrs.rb +3 -33
  46. data/lib/geodetic/{coordinates → coordinate}/ned.rb +30 -1
  47. data/lib/geodetic/{coordinates → coordinate}/olc.rb +19 -225
  48. data/lib/geodetic/coordinate/spatial_hash.rb +342 -0
  49. data/lib/geodetic/{coordinates → coordinate}/state_plane.rb +30 -1
  50. data/lib/geodetic/{coordinates → coordinate}/ups.rb +3 -37
  51. data/lib/geodetic/{coordinates → coordinate}/usng.rb +3 -33
  52. data/lib/geodetic/{coordinates → coordinate}/utm.rb +3 -33
  53. data/lib/geodetic/{coordinates → coordinate}/web_mercator.rb +3 -33
  54. data/lib/geodetic/{coordinates.rb → coordinate.rb} +62 -45
  55. data/lib/geodetic/version.rb +1 -1
  56. data/lib/geodetic.rb +1 -1
  57. data/spatial_hash_idea.md +241 -0
  58. metadata +29 -20
  59. data/lib/geodetic/coordinates/gh.rb +0 -372
  60. data/lib/geodetic/coordinates/ham.rb +0 -435
@@ -0,0 +1,221 @@
1
+ # Geodetic::Coordinate::GEOREF
2
+
3
+ ## World Geographic Reference System
4
+
5
+ GEOREF is a grid-based geocode developed by the US military and adopted by ICAO for air navigation and air defense reporting. It encodes latitude/longitude into a compact alphanumeric string using a false coordinate system that shifts longitude by +180 and latitude by +90 to make all values positive.
6
+
7
+ The encoding reads longitude first, then latitude at each level:
8
+
9
+ | Level | Characters | Description | Resolution |
10
+ |-------|-----------|-------------|------------|
11
+ | **Tile** | 1-2 | 15-degree grid (24 lng x 12 lat letters) | 15 degrees |
12
+ | **Degree** | 3-4 | 1-degree subdivision within tile (15 letters each) | 1 degree |
13
+ | **Minutes** | 5+ | Numeric digit pairs (lng digits, then lat digits) | Variable |
14
+
15
+ The character set uses 24 letters (A-Z, omitting I and O) for tiles, and 15 letters (A-Q, omitting I and O) for degree subdivisions. Minute pairs are decimal digits.
16
+
17
+ Valid code lengths: 2, 4, 8, 10, 12, 14 characters (not 6 -- minimum numeric portion is 2 digits per axis).
18
+
19
+ GEOREF is a **2D coordinate system** (no altitude). Conversions to/from other systems go through LLA as the intermediary. Each code represents a rectangular cell; the coordinate's point value is the cell's midpoint.
20
+
21
+ ## Constructor
22
+
23
+ ```ruby
24
+ # From a GEOREF string
25
+ coord = Geodetic::Coordinate::GEOREF.new("GJPJ3417")
26
+
27
+ # From any coordinate (converts via LLA)
28
+ coord = Geodetic::Coordinate::GEOREF.new(lla_coord)
29
+ coord = Geodetic::Coordinate::GEOREF.new(utm_coord, precision: 10)
30
+ ```
31
+
32
+ | Parameter | Type | Default | Description |
33
+ |-------------|-------------------|---------|------------------------------------------------|
34
+ | `source` | String or Coord | -- | A GEOREF string or any coordinate object |
35
+ | `precision` | Integer | 8 | Code length: 2, 4, 8, 10, 12, or 14 |
36
+
37
+ Raises `ArgumentError` if the source string is empty, has an invalid length (including 6), or contains invalid characters. String input is case-insensitive (normalized to uppercase).
38
+
39
+ ## Attributes
40
+
41
+ | Attribute | Type | Access | Description |
42
+ |-----------|--------|-----------|--------------------------|
43
+ | `code` | String | read-only | The GEOREF code string |
44
+
45
+ GEOREF is **immutable** -- there are no setter methods.
46
+
47
+ ## Precision
48
+
49
+ The precision (code length) determines the size of the encoded cell:
50
+
51
+ | Length | Resolution | Approximate Cell Size |
52
+ |--------|-----------|----------------------|
53
+ | 2 | 15 degrees | ~1,668 km x 1,668 km |
54
+ | 4 | 1 degree | ~111 km x 111 km |
55
+ | 8 | 1 minute | ~1.85 km x 1.85 km |
56
+ | 10 | 0.1 minute | ~185 m x 185 m |
57
+ | 12 | 0.01 minute | ~18.5 m x 18.5 m |
58
+ | 14 | 0.001 minute | ~1.85 m x 1.85 m |
59
+
60
+ ```ruby
61
+ coord.precision # => 8 (code length)
62
+ coord.precision_in_meters # => { lat: ~1850, lng: ~1850 }
63
+ ```
64
+
65
+ ## Conversions
66
+
67
+ All conversions chain through LLA. The datum parameter defaults to `Geodetic::WGS84`.
68
+
69
+ ### Instance Methods
70
+
71
+ ```ruby
72
+ coord.to_lla # => LLA (midpoint of the cell)
73
+ coord.to_ecef
74
+ coord.to_utm
75
+ coord.to_enu(reference_lla)
76
+ coord.to_ned(reference_lla)
77
+ coord.to_mgrs
78
+ coord.to_usng
79
+ coord.to_web_mercator
80
+ coord.to_ups
81
+ coord.to_state_plane(zone_code)
82
+ coord.to_bng
83
+ coord.to_gh36
84
+ coord.to_gh
85
+ coord.to_ham
86
+ coord.to_olc
87
+ coord.to_gars
88
+ ```
89
+
90
+ ### Class Methods
91
+
92
+ ```ruby
93
+ GEOREF.from_lla(lla_coord)
94
+ GEOREF.from_ecef(ecef_coord)
95
+ GEOREF.from_utm(utm_coord)
96
+ GEOREF.from_web_mercator(wm_coord)
97
+ GEOREF.from_gh(gh_coord)
98
+ GEOREF.from_gars(gars_coord)
99
+ # ... and all other coordinate systems
100
+ ```
101
+
102
+ ### LLA Convenience Methods
103
+
104
+ ```ruby
105
+ lla = Geodetic::Coordinate::LLA.new(lat: 40.7128, lng: -74.0060)
106
+ georef = lla.to_georef # default precision 8
107
+ georef = lla.to_georef(precision: 10) # 0.1-minute precision
108
+
109
+ lla = Geodetic::Coordinate::LLA.from_georef(georef)
110
+ ```
111
+
112
+ ## Serialization
113
+
114
+ ### `to_s(truncate_to = nil)`
115
+
116
+ Returns the GEOREF string. An optional integer truncates to that precision (re-encodes from decoded coordinates).
117
+
118
+ ```ruby
119
+ coord = GEOREF.new("GJPJ3417")
120
+ coord.to_s # => "GJPJ3417"
121
+ coord.to_s(4) # => "GJPJ"
122
+ coord.to_s(2) # => "GJ"
123
+ ```
124
+
125
+ ### `to_slug`
126
+
127
+ Alias for `to_s`. GEOREF codes are already URL-safe.
128
+
129
+ ### `to_a`
130
+
131
+ Returns `[lat, lng]` of the cell midpoint.
132
+
133
+ ```ruby
134
+ coord.to_a # => [38.286..., -76.411...]
135
+ ```
136
+
137
+ ### `from_string` / `from_array`
138
+
139
+ ```ruby
140
+ GEOREF.from_string("GJPJ3417") # from GEOREF string
141
+ GEOREF.from_array([38.0, -76.0]) # from [lat, lng]
142
+ ```
143
+
144
+ ## Neighbors
145
+
146
+ Returns all 8 adjacent cells as GEOREF instances.
147
+
148
+ ```ruby
149
+ coord = GEOREF.new("GJPJ3417")
150
+ neighbors = coord.neighbors
151
+ # => { N: GEOREF, S: GEOREF, E: GEOREF, W: GEOREF, NE: GEOREF, NW: GEOREF, SE: GEOREF, SW: GEOREF }
152
+
153
+ neighbors[:N].to_lla.lat > coord.to_lla.lat # => true
154
+ neighbors[:E].to_lla.lng > coord.to_lla.lng # => true
155
+ ```
156
+
157
+ Neighbors preserve the same precision as the original code. Latitude is clamped to valid range near the poles; longitude wraps at the antimeridian.
158
+
159
+ ## Area
160
+
161
+ The `to_area` method returns the GEOREF cell as a `Geodetic::Areas::Rectangle`.
162
+
163
+ ```ruby
164
+ area = coord.to_area
165
+ # => Geodetic::Areas::Rectangle
166
+
167
+ area.includes?(coord.to_lla) # => true (midpoint is inside the cell)
168
+ area.nw # => LLA (northwest corner)
169
+ area.se # => LLA (southeast corner)
170
+ ```
171
+
172
+ ## Equality
173
+
174
+ Two GEOREF instances are equal if their code strings match exactly.
175
+
176
+ ```ruby
177
+ GEOREF.new("GJPJ3417") == GEOREF.new("GJPJ3417") # => true
178
+ GEOREF.new("GJPJ3417") == GEOREF.new("GJPJ3418") # => false
179
+ ```
180
+
181
+ ## `valid?`
182
+
183
+ Returns `true` if the code has a valid length (2, 4, 8, 10, 12, or 14), valid tile letters, valid degree letters, and properly formatted minute digits.
184
+
185
+ ```ruby
186
+ coord.valid? # => true
187
+ ```
188
+
189
+ ## Universal Distance and Bearing Methods
190
+
191
+ GEOREF supports all universal distance and bearing methods via the `DistanceMethods` and `BearingMethods` mixins:
192
+
193
+ ```ruby
194
+ a = GEOREF.new("GJPJ3417")
195
+ b = GEOREF.new("HJAL4243")
196
+
197
+ a.distance_to(b) # => Distance
198
+ a.straight_line_distance_to(b) # => Distance
199
+ a.bearing_to(b) # => Bearing
200
+ a.elevation_to(b) # => Float (degrees)
201
+ ```
202
+
203
+ ## Character Sets
204
+
205
+ **Tile longitude** (24 letters): `A B C D E F G H J K L M N P Q R S T U V W X Y Z`
206
+
207
+ **Tile latitude** (12 letters): `A B C D E F G H J K L M`
208
+
209
+ **Degree subdivision** (15 letters): `A B C D E F G H J K L M N P Q`
210
+
211
+ All sets omit `I` and `O` to avoid confusion with digits `1` and `0`.
212
+
213
+ ## Code Structure Examples
214
+
215
+ | Code | Meaning |
216
+ |------|---------|
217
+ | `GJ` | 15-degree tile (tile only) |
218
+ | `GJPJ` | 1-degree cell within tile |
219
+ | `GJPJ3417` | 1-minute cell (lng 34', lat 17') |
220
+ | `GJPJ342171` | 0.1-minute cell |
221
+ | `GJPJ34211712` | 0.01-minute cell |
@@ -1,4 +1,4 @@
1
- # Geodetic::Coordinates::GH
1
+ # Geodetic::Coordinate::GH
2
2
 
3
3
  ## Geohash (Base-32)
4
4
 
@@ -15,11 +15,11 @@ GH is a **2D coordinate system** (no altitude). Conversions to/from other system
15
15
 
16
16
  ```ruby
17
17
  # From a geohash string
18
- coord = Geodetic::Coordinates::GH.new("dr5ru7")
18
+ coord = Geodetic::Coordinate::GH.new("dr5ru7")
19
19
 
20
20
  # From any coordinate (converts via LLA)
21
- coord = Geodetic::Coordinates::GH.new(lla_coord)
22
- coord = Geodetic::Coordinates::GH.new(utm_coord, precision: 8)
21
+ coord = Geodetic::Coordinate::GH.new(lla_coord)
22
+ coord = Geodetic::Coordinate::GH.new(utm_coord, precision: 8)
23
23
  ```
24
24
 
25
25
  | Parameter | Type | Default | Description |
@@ -91,11 +91,11 @@ GH.from_gh36(gh36_coord)
91
91
  ### LLA Convenience Methods
92
92
 
93
93
  ```ruby
94
- lla = Geodetic::Coordinates::LLA.new(lat: 40.689167, lng: -74.044444)
94
+ lla = Geodetic::Coordinate::LLA.new(lat: 40.689167, lng: -74.044444)
95
95
  gh = lla.to_gh # default precision 12
96
96
  gh = lla.to_gh(precision: 6) # custom precision
97
97
 
98
- lla = Geodetic::Coordinates::LLA.from_gh(gh)
98
+ lla = Geodetic::Coordinate::LLA.from_gh(gh)
99
99
  ```
100
100
 
101
101
  ## Serialization
@@ -270,7 +270,7 @@ GH (base-32) is the de facto standard. Virtually all spatial databases and servi
270
270
  ### Converting Between GH and GH36
271
271
 
272
272
  ```ruby
273
- gh = Geodetic::Coordinates::GH.new("dr5ru7")
273
+ gh = Geodetic::Coordinate::GH.new("dr5ru7")
274
274
  gh36 = gh.to_gh36 # => GH36 instance
275
275
  gh_back = gh36.to_gh # => GH instance
276
276
 
@@ -1,4 +1,4 @@
1
- # Geodetic::Coordinates::GH36
1
+ # Geodetic::Coordinate::GH36
2
2
 
3
3
  ## Geohash-36
4
4
 
@@ -13,11 +13,11 @@ GH36 is a **2D coordinate system** (no altitude). Conversions to/from other syst
13
13
 
14
14
  ```ruby
15
15
  # From a geohash string
16
- coord = Geodetic::Coordinates::GH36.new("bdrdC26BqH")
16
+ coord = Geodetic::Coordinate::GH36.new("bdrdC26BqH")
17
17
 
18
18
  # From any coordinate (converts via LLA)
19
- coord = Geodetic::Coordinates::GH36.new(lla_coord)
20
- coord = Geodetic::Coordinates::GH36.new(utm_coord, precision: 8)
19
+ coord = Geodetic::Coordinate::GH36.new(lla_coord)
20
+ coord = Geodetic::Coordinate::GH36.new(utm_coord, precision: 8)
21
21
  ```
22
22
 
23
23
  | Parameter | Type | Default | Description |
@@ -86,11 +86,11 @@ GH36.from_web_mercator(wm_coord)
86
86
  ### LLA Convenience Methods
87
87
 
88
88
  ```ruby
89
- lla = Geodetic::Coordinates::LLA.new(lat: 40.689167, lng: -74.044444)
89
+ lla = Geodetic::Coordinate::LLA.new(lat: 40.689167, lng: -74.044444)
90
90
  gh36 = lla.to_gh36 # default precision 10
91
91
  gh36 = lla.to_gh36(precision: 5) # custom precision
92
92
 
93
- lla = Geodetic::Coordinates::LLA.from_gh36(gh36)
93
+ lla = Geodetic::Coordinate::LLA.from_gh36(gh36)
94
94
  ```
95
95
 
96
96
  ## Serialization
@@ -0,0 +1,312 @@
1
+ # Geodetic::Coordinate::H3
2
+
3
+ ## H3 Hexagonal Hierarchical Index
4
+
5
+ H3 is Uber's hierarchical geospatial indexing system that divides the globe into hexagonal cells (and 12 pentagons per resolution level) using an icosahedron projection. Each cell is identified by a 64-bit integer, typically displayed as a 15-character hex string like `872a1072bffffff`.
6
+
7
+ **H3 requires the `libh3` C library** installed on your system. Without it, all other coordinate systems work normally; H3 operations raise a clear error message with installation instructions.
8
+
9
+ ### Prerequisites
10
+
11
+ ```bash
12
+ # macOS (Homebrew)
13
+ brew install h3
14
+
15
+ # Linux (build from source)
16
+ git clone https://github.com/uber/h3.git
17
+ cd h3
18
+ cmake -B build -DCMAKE_INSTALL_PREFIX=/usr/local
19
+ cmake --build build
20
+ sudo cmake --install build
21
+ ```
22
+
23
+ You can also set the `LIBH3_PATH` environment variable to specify a custom library path:
24
+
25
+ ```bash
26
+ export LIBH3_PATH=/path/to/libh3.dylib
27
+ ```
28
+
29
+ Geodetic searches these paths automatically:
30
+ - `/opt/homebrew/lib/libh3.dylib` (macOS ARM Homebrew)
31
+ - `/usr/local/lib/libh3.dylib` (macOS Intel Homebrew)
32
+ - `/usr/lib/libh3.so` (Linux system)
33
+ - `/usr/local/lib/libh3.so` (Linux local install)
34
+
35
+ ### Key Differences from Other Spatial Hashes
36
+
37
+ | Feature | GH/OLC/GARS/GEOREF/HAM | H3 |
38
+ |---------|------------------------|-----|
39
+ | Cell shape | Rectangle | Hexagon (6 vertices) |
40
+ | `to_area` returns | `Areas::Rectangle` | `Areas::Polygon` |
41
+ | `neighbors` returns | Hash with 8 cardinal keys | Array of 6 cells |
42
+ | Code format | String | 64-bit integer (hex string) |
43
+ | Dependency | None (pure Ruby) | `libh3` (C library via fiddle) |
44
+
45
+ H3 is a **2D coordinate system** (no altitude). Conversions to/from other systems go through LLA as the intermediary. Each hex string represents a hexagonal cell; the coordinate's point value is the cell's centroid.
46
+
47
+ ## Constructor
48
+
49
+ ```ruby
50
+ # From a hex string
51
+ coord = Geodetic::Coordinate::H3.new("872a1072bffffff")
52
+
53
+ # From a hex string with 0x prefix
54
+ coord = Geodetic::Coordinate::H3.new("0x872a1072bffffff")
55
+
56
+ # From a 64-bit integer
57
+ coord = Geodetic::Coordinate::H3.new(0x872a1072bffffff)
58
+
59
+ # From any coordinate (converts via LLA)
60
+ coord = Geodetic::Coordinate::H3.new(lla_coord)
61
+ coord = Geodetic::Coordinate::H3.new(utm_coord, precision: 9)
62
+ ```
63
+
64
+ | Parameter | Type | Default | Description |
65
+ |-------------|-------------------------|---------|--------------------------------------------|
66
+ | `source` | String, Integer, or Coord | -- | An H3 hex string, integer, or coordinate |
67
+ | `precision` | Integer | 7 | H3 resolution level (0-15) |
68
+
69
+ Raises `ArgumentError` if the source string is empty, contains invalid hex characters, or does not represent a valid H3 cell index. String input is case-insensitive (normalized to lowercase). The `0x` prefix is stripped automatically.
70
+
71
+ ## Attributes
72
+
73
+ | Attribute | Type | Access | Description |
74
+ |------------|---------|-----------|----------------------------------|
75
+ | `code` | String | read-only | The hex string representation |
76
+ | `h3_index` | Integer | read-only | The 64-bit H3 cell index |
77
+
78
+ H3 is **immutable** -- there are no setter methods.
79
+
80
+ ## Resolution
81
+
82
+ H3 uses "resolution" (0-15) instead of string-length precision. Higher resolution means smaller cells.
83
+
84
+ | Resolution | Approximate Cell Area | Approximate Edge Length |
85
+ |------------|----------------------|------------------------|
86
+ | 0 | 4,357,449 km^2 | 1,108 km |
87
+ | 1 | 609,788 km^2 | 419 km |
88
+ | 2 | 86,801 km^2 | 158 km |
89
+ | 3 | 12,393 km^2 | 60 km |
90
+ | 4 | 1,770 km^2 | 23 km |
91
+ | 5 | 252 km^2 | 8.5 km |
92
+ | 6 | 36 km^2 | 3.2 km |
93
+ | 7 | 5.2 km^2 (default) | 1.2 km |
94
+ | 8 | 0.74 km^2 | 461 m |
95
+ | 9 | 0.105 km^2 | 174 m |
96
+ | 10 | 0.015 km^2 | 66 m |
97
+ | 11 | 0.002 km^2 | 25 m |
98
+ | 12 | 307 m^2 | 9.4 m |
99
+ | 13 | 43 m^2 | 3.6 m |
100
+ | 14 | 6.2 m^2 | 1.3 m |
101
+ | 15 | 0.9 m^2 | 0.5 m |
102
+
103
+ ```ruby
104
+ coord.resolution # => 7 (alias: coord.precision)
105
+ coord.cell_area # => 5182586.98 (square meters)
106
+ coord.precision_in_meters # => { lat: ~2276, lng: ~2276, area_m2: ~5182586 }
107
+ ```
108
+
109
+ ## Checking Availability
110
+
111
+ ```ruby
112
+ Geodetic::Coordinate::H3.available? # => true if libh3 is found
113
+ ```
114
+
115
+ ## Conversions
116
+
117
+ All conversions chain through LLA. The datum parameter defaults to `Geodetic::WGS84`.
118
+
119
+ ### Instance Methods
120
+
121
+ ```ruby
122
+ coord.to_lla # => LLA (centroid of the cell)
123
+ coord.to_ecef
124
+ coord.to_utm
125
+ coord.to_enu(reference_lla)
126
+ coord.to_ned(reference_lla)
127
+ coord.to_mgrs
128
+ coord.to_usng
129
+ coord.to_web_mercator
130
+ coord.to_ups
131
+ coord.to_state_plane(zone_code)
132
+ coord.to_bng
133
+ coord.to_gh36
134
+ coord.to_gh
135
+ coord.to_ham
136
+ coord.to_olc
137
+ coord.to_georef
138
+ coord.to_gars
139
+ ```
140
+
141
+ ### Class Methods
142
+
143
+ ```ruby
144
+ H3.from_lla(lla_coord)
145
+ H3.from_ecef(ecef_coord)
146
+ H3.from_utm(utm_coord)
147
+ H3.from_web_mercator(wm_coord)
148
+ H3.from_gh(gh_coord)
149
+ H3.from_georef(georef_coord)
150
+ H3.from_gars(gars_coord)
151
+ # ... and all other coordinate systems
152
+ ```
153
+
154
+ ### LLA Convenience Methods
155
+
156
+ ```ruby
157
+ lla = Geodetic::Coordinate::LLA.new(lat: 40.689167, lng: -74.044444)
158
+ h3 = lla.to_h3 # default resolution 7
159
+ h3 = lla.to_h3(precision: 9) # resolution 9
160
+
161
+ lla = Geodetic::Coordinate::LLA.from_h3(h3)
162
+ ```
163
+
164
+ ## Serialization
165
+
166
+ ### `to_s(format = nil)`
167
+
168
+ Returns the hex string. Pass `:integer` to get the 64-bit integer value.
169
+
170
+ ```ruby
171
+ coord = H3.new("872a1072bffffff")
172
+ coord.to_s # => "872a1072bffffff"
173
+ coord.to_s(:integer) # => 608693941536498687
174
+ coord.h3_index # => 608693941536498687
175
+ ```
176
+
177
+ ### `to_slug`
178
+
179
+ Alias for `to_s`. H3 hex strings are already URL-safe.
180
+
181
+ ### `to_a`
182
+
183
+ Returns `[lat, lng]` of the cell centroid.
184
+
185
+ ```ruby
186
+ coord.to_a # => [40.685..., -74.030...]
187
+ ```
188
+
189
+ ### `from_string` / `from_array`
190
+
191
+ ```ruby
192
+ H3.from_string("872a1072bffffff") # from hex string
193
+ H3.from_array([40.689167, -74.044444]) # from [lat, lng]
194
+ ```
195
+
196
+ ## Neighbors
197
+
198
+ Returns all adjacent cells as an Array of H3 instances. Hexagons have 6 neighbors; pentagons have 5.
199
+
200
+ Note: unlike the rectangular spatial hashes which return a directional Hash (`:N`, `:S`, etc.), H3 returns a flat Array because hexagonal cells do not have cardinal directions.
201
+
202
+ ```ruby
203
+ coord = H3.new("872a1072bffffff")
204
+ neighbors = coord.neighbors
205
+ # => [H3, H3, H3, H3, H3, H3]
206
+
207
+ neighbors.length # => 6
208
+ ```
209
+
210
+ ## Grid Disk
211
+
212
+ The `grid_disk(k)` method returns all cells within `k` steps. This is a generalization of `neighbors` (which is `grid_disk(1)` minus self).
213
+
214
+ ```ruby
215
+ coord.grid_disk(0) # => [self] (1 cell)
216
+ coord.grid_disk(1) # => [self + 6 neighbors] (7 cells)
217
+ coord.grid_disk(2) # => 19 cells
218
+ ```
219
+
220
+ ## Parent and Children
221
+
222
+ Navigate the H3 hierarchy by moving to coarser or finer resolution levels.
223
+
224
+ ```ruby
225
+ coord = H3.new("872a1072bffffff") # resolution 7
226
+ parent = coord.parent(5) # => H3 at resolution 5
227
+ parent.resolution # => 5
228
+
229
+ children = coord.children(8) # => Array of 7 H3 cells at resolution 8
230
+ children.length # => 7
231
+ children.first.resolution # => 8
232
+ ```
233
+
234
+ `parent` raises `ArgumentError` if the target resolution is not coarser (lower number). `children` raises `ArgumentError` if the target resolution is not finer (higher number).
235
+
236
+ ## Area
237
+
238
+ The `to_area` method returns the hexagonal cell boundary as an `Areas::Polygon` with 6 vertices (5 for pentagons).
239
+
240
+ ```ruby
241
+ area = coord.to_area
242
+ # => Geodetic::Areas::Polygon
243
+
244
+ area.includes?(coord.to_lla) # => true (centroid is inside the cell)
245
+ area.boundary.length # => 7 (6 vertices + closing point)
246
+ ```
247
+
248
+ ## Pentagon Detection
249
+
250
+ 12 cells at each resolution level are pentagons (artifacts of the icosahedral projection). These have 5 neighbors and 5 boundary vertices instead of 6.
251
+
252
+ ```ruby
253
+ coord.pentagon? # => false (most cells are hexagons)
254
+ ```
255
+
256
+ ## Cell Area
257
+
258
+ ```ruby
259
+ coord.cell_area # => 5182586.98 (square meters)
260
+ ```
261
+
262
+ ## Equality
263
+
264
+ Two H3 instances are equal if their hex strings match exactly.
265
+
266
+ ```ruby
267
+ H3.new("872a1072bffffff") == H3.new("872a1072bffffff") # => true
268
+ H3.new("872a1072bffffff") == H3.new(0x872a1072bffffff) # => true (integer)
269
+ H3.new("872a1072bffffff") == H3.new("87195da49ffffff") # => false
270
+ ```
271
+
272
+ ## `valid?`
273
+
274
+ Returns `true` if the H3 cell index is valid according to the H3 library.
275
+
276
+ ```ruby
277
+ coord.valid? # => true
278
+ ```
279
+
280
+ ## Universal Distance and Bearing Methods
281
+
282
+ H3 supports all universal distance and bearing methods via the `DistanceMethods` and `BearingMethods` mixins:
283
+
284
+ ```ruby
285
+ a = H3.new("872a1072bffffff") # Statue of Liberty area
286
+ b = H3.new("87195da49ffffff") # London area
287
+
288
+ a.distance_to(b) # => Distance (~5,570 km)
289
+ a.straight_line_distance_to(b) # => Distance
290
+ a.bearing_to(b) # => Bearing
291
+ a.elevation_to(b) # => Float (degrees)
292
+ ```
293
+
294
+ ## Well-Known H3 Cells
295
+
296
+ | Location | H3 Index (res 7) | Resolution |
297
+ |----------|------------------|------------|
298
+ | Statue of Liberty | `872a1072bffffff` | 7 |
299
+ | London | `87195da49ffffff` | 7 |
300
+ | Null Island (0, 0) | `87754e64dffffff` | 7 |
301
+
302
+ ## Implementation Notes
303
+
304
+ Geodetic uses Ruby's `fiddle` (part of the standard library) to call the H3 v4 C API directly. No gem dependency beyond `fiddle` is required. The H3 C library must be installed separately.
305
+
306
+ The library search order is:
307
+ 1. `LIBH3_PATH` environment variable
308
+ 2. `/opt/homebrew/lib/libh3.dylib` (macOS ARM)
309
+ 3. `/usr/local/lib/libh3.dylib` (macOS Intel)
310
+ 4. `/usr/lib/libh3.so` (Linux)
311
+ 5. `/usr/local/lib/libh3.so` (Linux local)
312
+ 6. Architecture-specific Linux paths
@@ -1,4 +1,4 @@
1
- # Geodetic::Coordinates::HAM
1
+ # Geodetic::Coordinate::HAM
2
2
 
3
3
  ## Maidenhead Locator System
4
4
 
@@ -21,11 +21,11 @@ HAM is a **2D coordinate system** (no altitude). Conversions to/from other syste
21
21
 
22
22
  ```ruby
23
23
  # From a Maidenhead locator string
24
- coord = Geodetic::Coordinates::HAM.new("FN31pr")
24
+ coord = Geodetic::Coordinate::HAM.new("FN31pr")
25
25
 
26
26
  # From any coordinate (converts via LLA)
27
- coord = Geodetic::Coordinates::HAM.new(lla_coord)
28
- coord = Geodetic::Coordinates::HAM.new(utm_coord, precision: 8)
27
+ coord = Geodetic::Coordinate::HAM.new(lla_coord)
28
+ coord = Geodetic::Coordinate::HAM.new(utm_coord, precision: 8)
29
29
  ```
30
30
 
31
31
  | Parameter | Type | Default | Description |
@@ -98,11 +98,11 @@ HAM.from_olc(olc_coord)
98
98
  ### LLA Convenience Methods
99
99
 
100
100
  ```ruby
101
- lla = Geodetic::Coordinates::LLA.new(lat: 40.689167, lng: -74.044444)
101
+ lla = Geodetic::Coordinate::LLA.new(lat: 40.689167, lng: -74.044444)
102
102
  ham = lla.to_ham # default precision 6
103
103
  ham = lla.to_ham(precision: 8) # extended precision
104
104
 
105
- lla = Geodetic::Coordinates::LLA.from_ham(ham)
105
+ lla = Geodetic::Coordinate::LLA.from_ham(ham)
106
106
  ```
107
107
 
108
108
  ## Serialization