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.
- checksums.yaml +7 -0
- data/CHANGELOG.md +29 -0
- data/LICENSE.txt +21 -0
- data/README.md +416 -0
- data/app/controllers/overture_maps/application_controller.rb +28 -0
- data/app/controllers/overture_maps/attribution_controller.rb +15 -0
- data/app/controllers/overture_maps/features_controller.rb +125 -0
- data/app/controllers/overture_maps/search_controller.rb +31 -0
- data/app/controllers/overture_maps/tiles_controller.rb +85 -0
- data/config/routes.rb +13 -0
- data/exe/overture-maps-mcp +6 -0
- data/lib/generators/overture_maps/address_generator.rb +19 -0
- data/lib/generators/overture_maps/base_features_generator.rb +19 -0
- data/lib/generators/overture_maps/base_generator.rb +22 -0
- data/lib/generators/overture_maps/building_generator.rb +19 -0
- data/lib/generators/overture_maps/division_generator.rb +19 -0
- data/lib/generators/overture_maps/install_generator.rb +42 -0
- data/lib/generators/overture_maps/place_generator.rb +19 -0
- data/lib/generators/overture_maps/transportation_generator.rb +20 -0
- data/lib/generators/templates/address_migration.rb.tt +25 -0
- data/lib/generators/templates/address_model.rb.tt +4 -0
- data/lib/generators/templates/base_feature_model.rb.tt +4 -0
- data/lib/generators/templates/base_features_migration.rb.tt +27 -0
- data/lib/generators/templates/building_migration.rb.tt +24 -0
- data/lib/generators/templates/building_model.rb.tt +4 -0
- data/lib/generators/templates/category_migration.rb.tt +15 -0
- data/lib/generators/templates/category_model.rb.tt +4 -0
- data/lib/generators/templates/connector_model.rb.tt +4 -0
- data/lib/generators/templates/division_migration.rb.tt +31 -0
- data/lib/generators/templates/division_model.rb.tt +4 -0
- data/lib/generators/templates/imported_areas_migration.rb.tt +20 -0
- data/lib/generators/templates/place_migration.rb.tt +30 -0
- data/lib/generators/templates/place_model.rb.tt +4 -0
- data/lib/generators/templates/postgis_migration.rb.tt +5 -0
- data/lib/generators/templates/segment_model.rb.tt +4 -0
- data/lib/generators/templates/transportation_migration.rb.tt +38 -0
- data/lib/overture_maps/attribution.rb +66 -0
- data/lib/overture_maps/bounding_box.rb +80 -0
- data/lib/overture_maps/changelog.rb +70 -0
- data/lib/overture_maps/configuration.rb +43 -0
- data/lib/overture_maps/database.rb +74 -0
- data/lib/overture_maps/division_search.rb +44 -0
- data/lib/overture_maps/engine.rb +32 -0
- data/lib/overture_maps/geometry_parser.rb +41 -0
- data/lib/overture_maps/gers.rb +30 -0
- data/lib/overture_maps/import/downloader.rb +248 -0
- data/lib/overture_maps/import/location_based_runner.rb +176 -0
- data/lib/overture_maps/import/parquet_reader.rb +32 -0
- data/lib/overture_maps/import/record_mapper.rb +182 -0
- data/lib/overture_maps/import/runner.rb +125 -0
- data/lib/overture_maps/import.rb +27 -0
- data/lib/overture_maps/mcp_server.rb +222 -0
- data/lib/overture_maps/models/address.rb +32 -0
- data/lib/overture_maps/models/base.rb +39 -0
- data/lib/overture_maps/models/base_feature.rb +31 -0
- data/lib/overture_maps/models/building.rb +35 -0
- data/lib/overture_maps/models/category.rb +32 -0
- data/lib/overture_maps/models/connector.rb +11 -0
- data/lib/overture_maps/models/division.rb +43 -0
- data/lib/overture_maps/models/imported_area.rb +42 -0
- data/lib/overture_maps/models/place.rb +43 -0
- data/lib/overture_maps/models/segment.rb +22 -0
- data/lib/overture_maps/models.rb +25 -0
- data/lib/overture_maps/query.rb +168 -0
- data/lib/overture_maps/query_engine.rb +268 -0
- data/lib/overture_maps/releases.rb +74 -0
- data/lib/overture_maps/storage.rb +121 -0
- data/lib/overture_maps/syncer.rb +99 -0
- data/lib/overture_maps/util.rb +30 -0
- data/lib/overture_maps/version.rb +3 -0
- data/lib/overture_maps.rb +42 -0
- data/lib/tasks/overture_maps.rake +554 -0
- 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
|