overture_maps 0.1.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 (73) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +29 -0
  3. data/LICENSE.txt +21 -0
  4. data/README.md +416 -0
  5. data/app/controllers/overture_maps/application_controller.rb +28 -0
  6. data/app/controllers/overture_maps/attribution_controller.rb +15 -0
  7. data/app/controllers/overture_maps/features_controller.rb +125 -0
  8. data/app/controllers/overture_maps/search_controller.rb +31 -0
  9. data/app/controllers/overture_maps/tiles_controller.rb +85 -0
  10. data/config/routes.rb +13 -0
  11. data/exe/overture-maps-mcp +6 -0
  12. data/lib/generators/overture_maps/address_generator.rb +19 -0
  13. data/lib/generators/overture_maps/base_features_generator.rb +19 -0
  14. data/lib/generators/overture_maps/base_generator.rb +22 -0
  15. data/lib/generators/overture_maps/building_generator.rb +19 -0
  16. data/lib/generators/overture_maps/division_generator.rb +19 -0
  17. data/lib/generators/overture_maps/install_generator.rb +42 -0
  18. data/lib/generators/overture_maps/place_generator.rb +19 -0
  19. data/lib/generators/overture_maps/transportation_generator.rb +20 -0
  20. data/lib/generators/templates/address_migration.rb.tt +25 -0
  21. data/lib/generators/templates/address_model.rb.tt +4 -0
  22. data/lib/generators/templates/base_feature_model.rb.tt +4 -0
  23. data/lib/generators/templates/base_features_migration.rb.tt +27 -0
  24. data/lib/generators/templates/building_migration.rb.tt +24 -0
  25. data/lib/generators/templates/building_model.rb.tt +4 -0
  26. data/lib/generators/templates/category_migration.rb.tt +15 -0
  27. data/lib/generators/templates/category_model.rb.tt +4 -0
  28. data/lib/generators/templates/connector_model.rb.tt +4 -0
  29. data/lib/generators/templates/division_migration.rb.tt +31 -0
  30. data/lib/generators/templates/division_model.rb.tt +4 -0
  31. data/lib/generators/templates/imported_areas_migration.rb.tt +20 -0
  32. data/lib/generators/templates/place_migration.rb.tt +30 -0
  33. data/lib/generators/templates/place_model.rb.tt +4 -0
  34. data/lib/generators/templates/postgis_migration.rb.tt +5 -0
  35. data/lib/generators/templates/segment_model.rb.tt +4 -0
  36. data/lib/generators/templates/transportation_migration.rb.tt +38 -0
  37. data/lib/overture_maps/attribution.rb +66 -0
  38. data/lib/overture_maps/bounding_box.rb +80 -0
  39. data/lib/overture_maps/changelog.rb +70 -0
  40. data/lib/overture_maps/configuration.rb +43 -0
  41. data/lib/overture_maps/database.rb +74 -0
  42. data/lib/overture_maps/division_search.rb +44 -0
  43. data/lib/overture_maps/engine.rb +32 -0
  44. data/lib/overture_maps/geometry_parser.rb +41 -0
  45. data/lib/overture_maps/gers.rb +30 -0
  46. data/lib/overture_maps/import/downloader.rb +248 -0
  47. data/lib/overture_maps/import/location_based_runner.rb +176 -0
  48. data/lib/overture_maps/import/parquet_reader.rb +32 -0
  49. data/lib/overture_maps/import/record_mapper.rb +182 -0
  50. data/lib/overture_maps/import/runner.rb +125 -0
  51. data/lib/overture_maps/import.rb +27 -0
  52. data/lib/overture_maps/mcp_server.rb +222 -0
  53. data/lib/overture_maps/models/address.rb +32 -0
  54. data/lib/overture_maps/models/base.rb +39 -0
  55. data/lib/overture_maps/models/base_feature.rb +31 -0
  56. data/lib/overture_maps/models/building.rb +35 -0
  57. data/lib/overture_maps/models/category.rb +32 -0
  58. data/lib/overture_maps/models/connector.rb +11 -0
  59. data/lib/overture_maps/models/division.rb +43 -0
  60. data/lib/overture_maps/models/imported_area.rb +42 -0
  61. data/lib/overture_maps/models/place.rb +43 -0
  62. data/lib/overture_maps/models/segment.rb +22 -0
  63. data/lib/overture_maps/models.rb +25 -0
  64. data/lib/overture_maps/query.rb +168 -0
  65. data/lib/overture_maps/query_engine.rb +268 -0
  66. data/lib/overture_maps/releases.rb +74 -0
  67. data/lib/overture_maps/storage.rb +121 -0
  68. data/lib/overture_maps/syncer.rb +99 -0
  69. data/lib/overture_maps/util.rb +30 -0
  70. data/lib/overture_maps/version.rb +3 -0
  71. data/lib/overture_maps.rb +42 -0
  72. data/lib/tasks/overture_maps.rake +554 -0
  73. metadata +301 -0
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: d14f7592823c00db641da88d4d89558758dfb67614a20b0d742401db4eabb233
4
+ data.tar.gz: 0112f6338b6245b8574328622300a44c8dfb83fd6d944be8cecd2ffc20daf27c
5
+ SHA512:
6
+ metadata.gz: c959c7e964b4117b26faebd4bebe2685fa0eb251268eaac80fc2248aa2b5de260c333c96cf32a9419aaf3b7916c793a8e45caaa1c63a831d90c9ba8dc54389eb
7
+ data.tar.gz: 18046c68c4c724de398c925a15c03eb0edd9593d48eacc1394540700633de524848b30c5a2e79303478b28f8576e586a6b8fe93120416e4e32714106080ac6b7
data/CHANGELOG.md ADDED
@@ -0,0 +1,29 @@
1
+ # Changelog
2
+
3
+ All notable changes to this project are documented in this file. The format
4
+ is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
5
+
6
+ ## [0.1.0] - 2026-07-12
7
+
8
+ Initial release.
9
+
10
+ ### Added
11
+
12
+ - Location-based imports for all six Overture themes (places, buildings,
13
+ addresses, divisions, transportation, base) into PostGIS: bbox-filtered
14
+ GeoParquet extracts via DuckDB with row-group pushdown, idempotent
15
+ `upsert_all` imports keyed on GERS ids, and shared extract caching.
16
+ - Division geocoding (`rails overture_maps:import:places[Seattle]`) with
17
+ local-first resolution once divisions are imported.
18
+ - Ad-hoc query API (`OvertureMaps.query`) over remote or cached GeoParquet —
19
+ counts, streaming, GeoJSON/GPKG exports — with no database required.
20
+ - Release discovery via Overture's STAC catalog; changelog-driven syncing
21
+ (`rails overture_maps:sync`) that applies removals and upserts instead of
22
+ full reloads; GERS registry lookups.
23
+ - Category taxonomy with group expansion (`eat_and_drink` → leaf categories).
24
+ - Mountable REST engine: JSON/GeoJSON feature endpoints with keyset
25
+ pagination, division search, license-correct attribution notices, and
26
+ Mapbox Vector Tiles straight from PostGIS (`ST_AsMVT`).
27
+ - `overture-maps-mcp`: a read-only MCP server over the public Overture
28
+ bucket (geocode, query/count features, GeoJSON export, GERS lookup).
29
+ - Install generator creating migrations, models, and the engine mount.
data/LICENSE.txt ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 Overture Maps
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,416 @@
1
+ # Overture Maps Ruby Gem
2
+
3
+ A Ruby gem for integrating with [Overture Maps](https://overturemaps.org/) — an open map data foundation providing geospatial data. Download bbox-filtered extracts of Overture GeoParquet data, import them into your Rails application's PostGIS database, serve them through ActiveRecord, a mountable REST API, or vector tiles, and keep them current with changelog-driven syncs. Or skip the database entirely: query the bucket ad hoc from Ruby, or hand AI assistants the bundled MCP server.
4
+
5
+ This is an unofficial community gem, not a product of the Overture Maps Foundation.
6
+
7
+ ## Features
8
+
9
+ - **Location-based import**: `rails overture_maps:import:places[Seattle]` — searches Overture divisions by name, downloads a bbox-filtered extract, and upserts it into PostGIS
10
+ - **All six Overture themes**: places, buildings, addresses, divisions, transportation, and base, with category filters backed by the full ~2,100-entry Overture taxonomy
11
+ - **Efficient remote access**: DuckDB pushes bbox predicates down to parquet row-group statistics, so only the relevant slice of Overture's multi-hundred-GB datasets is transferred
12
+ - **Idempotent imports**: rows are upserted by GERS id; re-running an import updates rather than fails
13
+ - **Release-aware**: discovers the latest monthly Overture release via the official STAC catalog; extracts are cached per release and area
14
+ - **Changelog-driven sync**: `rails overture_maps:sync` brings imported areas to the latest release by applying the official GERS changelog — no full reloads
15
+ - **REST API**: a mountable engine serves imported data as JSON, GeoJSON, and Mapbox Vector Tiles, so MapLibre renders straight from your Rails app with no separate tile server
16
+ - **Ad-hoc querying**: `OvertureMaps.query` counts, streams, and exports Overture data straight from the bucket — no import, no database
17
+ - **MCP server**: `overture-maps-mcp` gives AI assistants read-only geocoding, query, and GeoJSON export tools over Overture's public data
18
+ - **Rails integration**: model generators, spatial scopes, rake tasks, PostGIS utilities, RGeo geometries
19
+ - **Attribution helpers**: builds the ODbL/CDLA notices your map is required to display from the imported source metadata
20
+
21
+ ## Installation
22
+
23
+ Add to your Rails application's Gemfile:
24
+
25
+ ```ruby
26
+ gem "overture_maps"
27
+
28
+ # Recommended: native DuckDB bindings for the fastest remote queries.
29
+ # Without this the gem falls back to the duckdb CLI (PATH lookup, then a
30
+ # pinned download to ~/.cache/overture_maps).
31
+ gem "duckdb"
32
+ ```
33
+
34
+ Then run:
35
+
36
+ ```bash
37
+ bundle install
38
+ ```
39
+
40
+ ## Getting Started
41
+
42
+ ### Generate Models and Migrations
43
+
44
+ ```bash
45
+ rails generate overture_maps:install
46
+ rails db:migrate
47
+ ```
48
+
49
+ This creates migrations for the PostGIS extension and the `overture_places`, `overture_buildings`, `overture_addresses`, `overture_categories`, `overture_divisions`, `overture_segments`/`overture_connectors`, and `overture_base_features` tables, plus their model files. Individual generators also exist: `overture_maps:place`, `overture_maps:building`, `overture_maps:address`, `overture_maps:division`, `overture_maps:transportation`, `overture_maps:base_features`.
50
+
51
+ ### Fetch Categories (Recommended)
52
+
53
+ ```bash
54
+ rails overture_maps:categories:populate # ~2,100 categories from the Overture taxonomy
55
+ rails overture_maps:categories:list # list all
56
+ rails overture_maps:categories:primary # list primary categories
57
+ ```
58
+
59
+ ## Importing Data
60
+
61
+ ```bash
62
+ # By location name (searches Overture divisions; prompts if ambiguous)
63
+ rails overture_maps:import:places[Seattle]
64
+ rails "overture_maps:import:places[New York]"
65
+
66
+ # By bounding box (underscore-separated lat/lng pairs)
67
+ rails overture_maps:import:places[47.606_-122.336_47.609_-122.333]
68
+
69
+ # Filter places by Overture leaf categories
70
+ rails "overture_maps:import:places[Seattle,cafe]"
71
+ rails "overture_maps:import:places[Seattle,cafe restaurant]"
72
+
73
+ # Filter by taxonomy group (expands to leaf categories once
74
+ # overture_maps:categories:populate has run)
75
+ rails "overture_maps:import:places[Seattle,eat_and_drink]"
76
+
77
+ # Other themes
78
+ rails overture_maps:import:buildings[Seattle]
79
+ rails overture_maps:import:addresses[Seattle]
80
+ rails overture_maps:import:divisions[Washington]
81
+ rails overture_maps:import:transportation[Seattle]
82
+ rails overture_maps:import:base[Seattle]
83
+
84
+ # Everything (resolves the location once, then imports each theme)
85
+ rails overture_maps:import:all[Seattle]
86
+ ```
87
+
88
+ Importing divisions has a bonus: once `overture_divisions` is populated,
89
+ location-name searches resolve against your local database instead of
90
+ querying Overture's bucket — imports and searches get much faster.
91
+
92
+ How it works:
93
+
94
+ 1. The location is parsed as a bounding box, or matched against Overture division areas by name (you're prompted when several match).
95
+ 2. A bbox-filtered parquet extract is downloaded via DuckDB into `tmp/overture/`, named by theme, type, release, and area — reruns for the same release and area reuse it (you're prompted; non-interactive runs reuse automatically).
96
+ 3. Records are upserted in batches, keyed on their GERS id.
97
+
98
+ Useful companions:
99
+
100
+ ```bash
101
+ rails overture_maps:import:search[Seattle] # see matching divisions first
102
+ rails overture_maps:import:stats # row counts per table
103
+ OVERTURE_RELEASE=2026-05-21.0 rails overture_maps:import:places[Seattle] # pin a release
104
+ OVERTURE_NON_INTERACTIVE=1 ... # never prompt (jobs/CI)
105
+ IGNORE_ERRORS=1 ... # exit 0 despite row errors
106
+ VERBOSE=1 ... # print row error details
107
+ ```
108
+
109
+ ## Downloading Data (without importing)
110
+
111
+ ```bash
112
+ # Bbox extracts for a location, one file per feature type
113
+ rails overture_maps:download:places[Seattle]
114
+ rails overture_maps:download:buildings[47.606_-122.336_47.609_-122.333]
115
+
116
+ # Explicit bbox / point + radius
117
+ rails overture_maps:download:bbox[places,47.606,-122.336,47.609,-122.333]
118
+ rails overture_maps:download:nearby[places,47.6062,-122.3321,5000]
119
+
120
+ # Export formats other than parquet
121
+ rails "overture_maps:download:bbox[places,47.6,-122.4,47.7,-122.2,place,,,geojson]"
122
+
123
+ # Complete theme files (no location argument — very large!)
124
+ rails overture_maps:download:places
125
+
126
+ # Discovery
127
+ rails overture_maps:download:versions # available Overture releases
128
+ rails overture_maps:download:themes # themes and their feature types
129
+ rails overture_maps:download:types[buildings] # types present in the current release
130
+ rails overture_maps:download:list[places] # files without downloading
131
+ rails overture_maps:download:search_divisions[Seattle]
132
+ ```
133
+
134
+ ### Data Structure
135
+
136
+ Overture Maps data is organized by **theme** and **type**:
137
+
138
+ | Theme | Types |
139
+ |-------|-------|
140
+ | addresses | address |
141
+ | base | bathymetry, infrastructure, land, land_cover, land_use, water |
142
+ | buildings | building, building_part |
143
+ | divisions | division, division_area, division_boundary |
144
+ | places | place |
145
+ | transportation | connector, segment |
146
+
147
+ All six themes can be imported and downloaded. Imports cover the types shown above except `building_part` (planned; needs the parent-building relationship). Division imports use `division_area` — the geocodable territories.
148
+
149
+ ## REST API
150
+
151
+ The gem ships a mountable engine serving imported data as JSON, GeoJSON,
152
+ and Mapbox Vector Tiles (the install generator adds the mount line):
153
+
154
+ ```ruby
155
+ # config/routes.rb
156
+ mount OvertureMaps::Engine => "/overture"
157
+ ```
158
+
159
+ ```
160
+ GET /overture/places?bbox=-122.35,47.60,-122.33,47.62 # west,south,east,north
161
+ GET /overture/places?near=47.609,-122.34,500&category=cafe
162
+ GET /overture/buildings?q=tower&format=geojson # FeatureCollection
163
+ GET /overture/places/<gers-id>
164
+ GET /overture/search?q=Seattle # division geocoding
165
+ GET /overture/tiles/places/14/2624/5721.mvt # vector tiles via ST_AsMVT
166
+ ```
167
+
168
+ Resources: `places`, `buildings`, `addresses`, `divisions`, `segments`,
169
+ `connectors`, `base_features`. Collections paginate by keyset — pass
170
+ `meta.next_cursor` back as `?after=`. Limits are capped
171
+ (`config.api_max_limit`), tile responses carry public cache headers, and
172
+ everything is read-only.
173
+
174
+ MapLibre can render imported data with no separate tile server:
175
+
176
+ ```js
177
+ map.addSource("places", {
178
+ type: "vector",
179
+ tiles: ["https://your-app.example/overture/tiles/places/{z}/{x}/{y}.mvt"]
180
+ });
181
+ ```
182
+
183
+ The API is open by default; wrap it in your own auth:
184
+
185
+ ```ruby
186
+ OvertureMaps.configure do |config|
187
+ config.api_auth = ->(controller) {
188
+ controller.head :unauthorized unless controller.request.headers["X-Api-Key"] == Rails.application.credentials.overture_api_key
189
+ }
190
+ end
191
+ ```
192
+
193
+ For public deployments, add rate limiting (e.g. Rack::Attack) in the host app.
194
+
195
+ ## Keeping Data Current
196
+
197
+ Overture publishes a new release monthly. Every import records its area and
198
+ release in `overture_imported_areas`, and syncing applies the official GERS
199
+ changelog instead of a full reload — removed features are deleted, added and
200
+ changed ones are upserted:
201
+
202
+ ```bash
203
+ rails overture_maps:sync:status # which areas are behind
204
+ rails overture_maps:sync # bring everything to the latest release
205
+ rails overture_maps:sync[2026-06-17.0] # or a specific release
206
+ ```
207
+
208
+ Areas whose release is no longer in the catalog (Overture prunes old
209
+ releases) get a full refresh automatically. Individual features can be
210
+ traced through releases with the registry:
211
+
212
+ ```bash
213
+ rails overture_maps:gers:lookup[1ef5ffe6-cea9-4d4d-98f3-efbedfa4a8d7]
214
+ # first_seen, last_seen, last_changed, bbox, data file path
215
+ ```
216
+
217
+ ```ruby
218
+ OvertureMaps::GERS.valid_id?(id) # dashed UUID (current) or legacy 32-hex
219
+ OvertureMaps::GERS.lookup(id) # registry row or nil
220
+ OvertureMaps::Changelog.counts(theme: "places", type: "place", release: "2026-06-17.0")
221
+ ```
222
+
223
+ ## Ad-hoc Querying (no import needed)
224
+
225
+ Query Overture GeoParquet directly — DuckDB pushes the bbox filter down to
226
+ row-group statistics, so even against the remote bucket only the relevant
227
+ slice is read:
228
+
229
+ ```ruby
230
+ # Count without downloading anything
231
+ OvertureMaps.query(theme: "places", bbox: [47.5, -122.4, 47.7, -122.2]).count
232
+
233
+ # Stream records (geometry parsed to RGeo features)
234
+ OvertureMaps.query(theme: "places", location: "Seattle").limit(100).each do |record|
235
+ puts record.dig("names", "primary")
236
+ end
237
+
238
+ # Batches, GeoJSON, exports
239
+ query = OvertureMaps.query(theme: "buildings", bbox: "47.5,-122.4,47.7,-122.2")
240
+ query.each_batch(size: 500) { |batch| ... }
241
+ query.limit(1000).to_geojson # FeatureCollection hash
242
+ query.export("buildings.geojson") # or .gpkg / .geojsonseq / .parquet
243
+
244
+ # Multi-type themes need an explicit type
245
+ OvertureMaps.query(theme: "transportation", type: "segment", location: "Seattle").count
246
+ ```
247
+
248
+ Unlimited queries spool through the same cache files the import pipeline
249
+ uses (`config.cache_dir`), so a query warms the cache for a later import and
250
+ vice versa. Manage the cache with:
251
+
252
+ ```bash
253
+ rails overture_maps:cache:list
254
+ rails overture_maps:cache:clear # everything
255
+ rails overture_maps:cache:clear[seattle] # matching extracts only
256
+ ```
257
+
258
+ ## Configuration
259
+
260
+ ```ruby
261
+ # config/initializers/overture_maps.rb
262
+ OvertureMaps.configure do |config|
263
+ config.release = "2026-06-17.0" # pin a release (default: latest via STAC)
264
+ config.cache_dir = "tmp/overture" # where extracts are cached
265
+ config.batch_size = 1000 # import batch size
266
+ config.timeout = 30 # HTTP timeout in seconds
267
+ config.non_interactive = false # never prompt (also OVERTURE_NON_INTERACTIVE=1)
268
+
269
+ # Point at a mirror (e.g. MinIO on your LAN) instead of Overture's bucket:
270
+ # config.s3_uri = "s3://my-mirror-bucket"
271
+ # config.s3_http_url = "https://my-mirror.example.com"
272
+ end
273
+ ```
274
+
275
+ ## Programmatic Usage
276
+
277
+ ```ruby
278
+ # Location-based import (what the rake tasks use)
279
+ runner = OvertureMaps::Import::LocationBasedRunner.new(
280
+ theme: "places",
281
+ location: "Seattle", # or "47.6_-122.4_47.7_-122.2", or a BoundingBox
282
+ model_class: OverturePlace,
283
+ categories: ["cafe"]
284
+ ).run
285
+ runner.imported_count # => 1234
286
+
287
+ # File import with a custom transform (keyword or block)
288
+ OvertureMaps::Import.run!(
289
+ theme: "places",
290
+ model_class: OverturePlace,
291
+ file_path: "/path/to/places.parquet"
292
+ ) do |record|
293
+ { id: record["id"], name: record.dig("names", "primary"), ... }
294
+ end
295
+
296
+ # Division search
297
+ OvertureMaps::Import::Downloader.search_divisions(query: "Seattle")
298
+ # => [{ id:, name:, subtype:, country:, region:, bbox:, area_km2: }, ...]
299
+
300
+ # Read a local parquet extract
301
+ reader = OvertureMaps::Import::ParquetReader.new
302
+ reader.each_record(source: "/path/to/file.parquet") { |record| ... }
303
+ ```
304
+
305
+ ## Model Usage
306
+
307
+ ```ruby
308
+ # Spatial scopes (all models)
309
+ OverturePlace.within_bounds(47.5, -122.4, 47.7, -122.2) # south, west, north, east
310
+ OverturePlace.near(47.6062, -122.3321, 1000) # lat, lng, radius in meters
311
+ OverturePlace.first.to_geojson
312
+
313
+ # Places
314
+ OverturePlace.by_category("cafe") # primary or alternate leaf category
315
+ OverturePlace.by_brand("Starbucks")
316
+ OverturePlace.by_country("US")
317
+ OverturePlace.by_operating_status("open")
318
+ OverturePlace.min_confidence(0.8)
319
+
320
+ # Buildings
321
+ OvertureBuilding.by_height(min: 50, max: 100)
322
+ OvertureBuilding.by_floors(min: 10)
323
+ OvertureBuilding.by_class("apartments")
324
+ OvertureBuilding.with_height
325
+
326
+ # Addresses
327
+ OvertureAddress.by_country("US")
328
+ OvertureAddress.by_locality("Seattle")
329
+ OvertureAddress.by_postcode("98101")
330
+ OvertureAddress.first.full_address
331
+
332
+ # Divisions
333
+ OvertureDivision.by_subtype("locality")
334
+ OvertureDivision.search_by_name("Seattle").largest_first
335
+ OvertureDivision.first.to_bounding_box
336
+
337
+ # Transportation
338
+ OvertureSegment.roads.by_class("motorway")
339
+ OvertureSegment.rails
340
+ OvertureConnector.near(47.6062, -122.3321, 500)
341
+
342
+ # Base features
343
+ OvertureBaseFeature.water
344
+ OvertureBaseFeature.land_use.by_class("park")
345
+ ```
346
+
347
+ ## Database Utilities
348
+
349
+ ```ruby
350
+ OvertureMaps::Database.postgis_available?
351
+ OvertureMaps::Database.create_spatial_index(:overture_places)
352
+ OvertureMaps::Database.bounding_box_query(:overture_places, south, west, north, east)
353
+ OvertureMaps::Database.nearest_neighbors(:overture_places, lat, lng)
354
+ ```
355
+
356
+ ## MCP Server
357
+
358
+ The gem ships `overture-maps-mcp`, a read-only [MCP](https://modelcontextprotocol.io)
359
+ server over Overture's public data — no Rails app, no database. AI assistants
360
+ get tools to geocode place names, query and count features, export GeoJSON,
361
+ and look up GERS ids, all streaming from the bucket with bbox pushdown.
362
+
363
+ ```bash
364
+ gem install overture_maps mcp
365
+ ```
366
+
367
+ Claude Desktop config:
368
+
369
+ ```json
370
+ {
371
+ "mcpServers": {
372
+ "overture-maps": { "command": "overture-maps-mcp" }
373
+ }
374
+ }
375
+ ```
376
+
377
+ Tools: `geocode`, `query_features`, `count_features`, `export_geojson`,
378
+ `gers_lookup`, `list_releases`. The server is read-only and never touches
379
+ your application database.
380
+
381
+ ## Attribution
382
+
383
+ Overture data carries per-theme licenses (ODbL for OSM-derived themes, CDLA-Permissive-2.0 for places, per-source terms for addresses). The imported `sources` column preserves which upstream datasets contributed, and the gem turns that into the notices your app needs:
384
+
385
+ ```ruby
386
+ OvertureMaps::Attribution.notices
387
+ # => ["Overture Maps Foundation — overturemaps.org",
388
+ # "© OpenStreetMap contributors (ODbL)",
389
+ # "Data sources: Microsoft, meta"]
390
+ OvertureMaps::Attribution.text # one line for a map corner
391
+ ```
392
+
393
+ Map UIs can fetch the same via `GET /overture/attribution`. See [docs.overturemaps.org/attribution](https://docs.overturemaps.org/attribution/) for the authoritative requirements.
394
+
395
+ ## Requirements
396
+
397
+ - Ruby >= 3.0
398
+ - Rails >= 7.0
399
+ - PostgreSQL with PostGIS
400
+ - DuckDB (the `duckdb` gem, a `duckdb` binary on PATH, or automatic CLI download)
401
+
402
+ ## Development
403
+
404
+ ```bash
405
+ bundle install
406
+ bundle exec rspec
407
+ ```
408
+
409
+ ## License
410
+
411
+ MIT License — see LICENSE.txt
412
+
413
+ ## Links
414
+
415
+ - [Overture Maps](https://overturemaps.org/)
416
+ - [Overture Maps Documentation](https://docs.overturemaps.org/)
@@ -0,0 +1,28 @@
1
+ # frozen_string_literal: true
2
+
3
+ module OvertureMaps
4
+ class ApplicationController < ActionController::API
5
+ before_action :authenticate!
6
+
7
+ rescue_from ArgumentError do |error|
8
+ render json: { error: error.message }, status: :bad_request
9
+ end
10
+
11
+ private
12
+
13
+ # Hosts wrap the API in their own auth:
14
+ # OvertureMaps.configure do |c|
15
+ # c.api_auth = ->(controller) {
16
+ # controller.head :unauthorized unless controller.request.headers["X-Api-Key"] == ...
17
+ # }
18
+ # end
19
+ # Rendering or heading inside the hook halts the request.
20
+ def authenticate!
21
+ OvertureMaps.configuration.api_auth&.call(self)
22
+ end
23
+
24
+ def geojson?
25
+ params[:format] == "geojson" || request.headers["Accept"].to_s.include?("geo+json")
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ module OvertureMaps
4
+ # GET /overture/attribution — the notices required for the data this
5
+ # database actually holds. Map UIs can render these verbatim.
6
+ class AttributionController < ApplicationController
7
+ def index
8
+ expires_in 1.hour, public: true
9
+ render json: {
10
+ notices: Attribution.notices,
11
+ text: Attribution.text
12
+ }
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,125 @@
1
+ # frozen_string_literal: true
2
+
3
+ module OvertureMaps
4
+ # Read-only access to imported Overture data.
5
+ #
6
+ # GET /overture/places?bbox=-122.4,47.5,-122.2,47.7&category=cafe&limit=50
7
+ # GET /overture/places?near=47.6,-122.3,1000&format=geojson
8
+ # GET /overture/buildings/<gers-id>
9
+ #
10
+ # Collections paginate by keyset: pass meta.next_cursor back as ?after=.
11
+ # ?format=geojson (or Accept: application/geo+json) returns GeoJSON.
12
+ class FeaturesController < ApplicationController
13
+ RESOURCES = {
14
+ "places" => Models::Place,
15
+ "buildings" => Models::Building,
16
+ "addresses" => Models::Address,
17
+ "divisions" => Models::Division,
18
+ "segments" => Models::Segment,
19
+ "connectors" => Models::Connector,
20
+ "base_features" => Models::BaseFeature
21
+ }.freeze
22
+
23
+ MAX_RADIUS_METERS = 100_000
24
+
25
+ def index
26
+ scope = apply_filters(model.all)
27
+ scope = scope.where("id > ?", params[:after].to_s) if params[:after].present?
28
+ records = scope.order(:id).limit(page_size).to_a
29
+
30
+ if geojson?
31
+ render json: {
32
+ type: "FeatureCollection",
33
+ features: records.map(&:to_geojson)
34
+ }
35
+ else
36
+ render json: {
37
+ data: records.map { |record| serialize(record) },
38
+ meta: {
39
+ count: records.length,
40
+ next_cursor: records.length == page_size ? records.last&.id : nil
41
+ }
42
+ }
43
+ end
44
+ end
45
+
46
+ def show
47
+ record = model.find_by(id: params[:id])
48
+ return render json: { error: "not found" }, status: :not_found unless record
49
+
50
+ render json: geojson? ? record.to_geojson : serialize(record)
51
+ end
52
+
53
+ private
54
+
55
+ def model
56
+ RESOURCES.fetch(params[:resource])
57
+ end
58
+
59
+ def apply_filters(scope)
60
+ scope = filter_bbox(scope)
61
+ scope = filter_near(scope)
62
+ scope = filter_name(scope)
63
+ scope = filter_columns(scope)
64
+ scope = scope.by_category(params[:category].to_s.split(",")) if params[:category].present? && model == Models::Place
65
+ scope
66
+ end
67
+
68
+ def filter_bbox(scope)
69
+ return scope unless params[:bbox].present?
70
+
71
+ # GeoJSON bbox order: west,south,east,north
72
+ values = params[:bbox].split(",").map { |v| Float(v) }
73
+ raise ArgumentError, "bbox must be west,south,east,north" unless values.length == 4
74
+
75
+ west, south, east, north = values
76
+ unless south.between?(-90, 90) && north.between?(-90, 90) &&
77
+ west.between?(-180, 180) && east.between?(-180, 180)
78
+ raise ArgumentError, "bbox out of range"
79
+ end
80
+
81
+ scope.within_bounds(south, west, north, east)
82
+ end
83
+
84
+ def filter_near(scope)
85
+ return scope unless params[:near].present?
86
+
87
+ values = params[:near].split(",").map { |v| Float(v) }
88
+ raise ArgumentError, "near must be lat,lng[,radius_meters]" unless values.length.between?(2, 3)
89
+
90
+ lat, lng = values
91
+ radius = (values[2] || 1000).clamp(1, MAX_RADIUS_METERS)
92
+ raise ArgumentError, "near out of range" unless lat.between?(-90, 90) && lng.between?(-180, 180)
93
+
94
+ scope.near(lat, lng, radius)
95
+ end
96
+
97
+ def filter_name(scope)
98
+ return scope unless params[:q].present? && model.column_names.include?("name")
99
+
100
+ scope.where("name ILIKE ?", "%#{model.sanitize_sql_like(params[:q])}%")
101
+ end
102
+
103
+ def filter_columns(scope)
104
+ %w[country subtype operating_status].each do |column|
105
+ next unless params[column].present? && model.column_names.include?(column)
106
+
107
+ scope = scope.where(column => params[column])
108
+ end
109
+ scope
110
+ end
111
+
112
+ def page_size
113
+ requested = params[:limit].present? ? Integer(params[:limit]) : OvertureMaps.configuration.api_default_limit
114
+ raise ArgumentError, "limit must be positive" unless requested.positive?
115
+
116
+ [requested, OvertureMaps.configuration.api_max_limit].min
117
+ end
118
+
119
+ def serialize(record)
120
+ attributes = record.attributes.except("geometry")
121
+ attributes["geometry"] = record.geometry && RGeo::GeoJSON.encode(record.geometry)
122
+ attributes
123
+ end
124
+ end
125
+ end
@@ -0,0 +1,31 @@
1
+ # frozen_string_literal: true
2
+
3
+ module OvertureMaps
4
+ # Division geocoding: GET /overture/search?q=Seattle
5
+ # Resolves from the local overture_divisions table when populated,
6
+ # falling back to Overture's bucket.
7
+ class SearchController < ApplicationController
8
+ def index
9
+ query = params[:q].to_s.strip
10
+ return render json: { error: "q is required" }, status: :unprocessable_entity if query.empty?
11
+
12
+ results = DivisionSearch.search(query: query)
13
+ render json: {
14
+ data: results.map do |result|
15
+ {
16
+ id: result[:id],
17
+ name: result[:name],
18
+ subtype: result[:subtype],
19
+ country: result[:country],
20
+ region: result[:region],
21
+ area_km2: result[:area_km2],
22
+ bbox: [result[:bbox].min_lng, result[:bbox].min_lat,
23
+ result[:bbox].max_lng, result[:bbox].max_lat]
24
+ }
25
+ end
26
+ }
27
+ rescue OvertureMaps::Error => e
28
+ render json: { error: e.message }, status: :bad_gateway
29
+ end
30
+ end
31
+ end