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
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "overture_maps"
|
|
4
|
+
|
|
5
|
+
begin
|
|
6
|
+
require "mcp"
|
|
7
|
+
require "mcp/server/transports/stdio_transport"
|
|
8
|
+
rescue LoadError
|
|
9
|
+
abort "The MCP server needs the official mcp gem: gem install mcp"
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
require "json"
|
|
13
|
+
|
|
14
|
+
module OvertureMaps
|
|
15
|
+
# A read-only MCP (Model Context Protocol) server over Overture's public
|
|
16
|
+
# GeoParquet — no Rails app, no database. Queries stream through DuckDB
|
|
17
|
+
# with bbox pushdown, so nothing is bulk-downloaded.
|
|
18
|
+
#
|
|
19
|
+
# overture-maps-mcp # speaks MCP over stdio
|
|
20
|
+
#
|
|
21
|
+
# Claude Desktop config:
|
|
22
|
+
# { "mcpServers": { "overture-maps": { "command": "overture-maps-mcp" } } }
|
|
23
|
+
module MCPServer
|
|
24
|
+
MAX_FEATURES = 100
|
|
25
|
+
|
|
26
|
+
module Helpers
|
|
27
|
+
module_function
|
|
28
|
+
|
|
29
|
+
def text_response(payload)
|
|
30
|
+
MCP::Tool::Response.new([{ type: "text", text: JSON.pretty_generate(payload) }])
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def error_response(message)
|
|
34
|
+
MCP::Tool::Response.new([{ type: "text", text: JSON.generate(error: message) }], error: true)
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
# Accepts either a named-corner bbox or a location name.
|
|
38
|
+
def resolve_bbox(args)
|
|
39
|
+
if args[:west] && args[:south] && args[:east] && args[:north]
|
|
40
|
+
BoundingBox.new(lat1: args[:south], lng1: args[:west],
|
|
41
|
+
lat2: args[:north], lng2: args[:east])
|
|
42
|
+
elsif args[:location]
|
|
43
|
+
results = DivisionSearch.search(query: args[:location])
|
|
44
|
+
raise Error, "no divisions found matching #{args[:location].inspect}" if results.empty?
|
|
45
|
+
|
|
46
|
+
results.first[:bbox]
|
|
47
|
+
else
|
|
48
|
+
raise ArgumentError, "provide west/south/east/north or a location name"
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def bbox_properties
|
|
53
|
+
{
|
|
54
|
+
location: { type: "string", description: "Place name to geocode (e.g. 'Seattle'); alternative to the bbox corners" },
|
|
55
|
+
west: { type: "number" }, south: { type: "number" },
|
|
56
|
+
east: { type: "number" }, north: { type: "number" }
|
|
57
|
+
}
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
class GeocodeTool < MCP::Tool
|
|
62
|
+
tool_name "geocode"
|
|
63
|
+
description "Find geographic divisions (countries, regions, cities, neighborhoods) by name. " \
|
|
64
|
+
"Returns each match with its bounding box [west, south, east, north]."
|
|
65
|
+
input_schema(
|
|
66
|
+
properties: { query: { type: "string", description: "Division name, e.g. 'Seattle'" } },
|
|
67
|
+
required: ["query"]
|
|
68
|
+
)
|
|
69
|
+
|
|
70
|
+
def self.call(query:, server_context: nil)
|
|
71
|
+
results = DivisionSearch.search(query: query).map do |r|
|
|
72
|
+
{
|
|
73
|
+
name: r[:name], subtype: r[:subtype], country: r[:country], region: r[:region],
|
|
74
|
+
area_km2: r[:area_km2],
|
|
75
|
+
bbox: [r[:bbox].min_lng, r[:bbox].min_lat, r[:bbox].max_lng, r[:bbox].max_lat]
|
|
76
|
+
}
|
|
77
|
+
end
|
|
78
|
+
Helpers.text_response(results)
|
|
79
|
+
rescue OvertureMaps::Error, ArgumentError => e
|
|
80
|
+
Helpers.error_response(e.message)
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
class QueryFeaturesTool < MCP::Tool
|
|
85
|
+
tool_name "query_features"
|
|
86
|
+
description "Fetch Overture Maps features (places, buildings, addresses, roads, ...) in an " \
|
|
87
|
+
"area as GeoJSON features. Themes: places, buildings, addresses, divisions, " \
|
|
88
|
+
"transportation (type segment/connector), base (type water/land/land_use/...). " \
|
|
89
|
+
"Limit is capped at #{MAX_FEATURES}."
|
|
90
|
+
input_schema(
|
|
91
|
+
properties: Helpers.bbox_properties.merge(
|
|
92
|
+
theme: { type: "string", description: "Overture theme, e.g. 'places'" },
|
|
93
|
+
type: { type: "string", description: "Feature type for multi-type themes, e.g. 'segment'" },
|
|
94
|
+
category: { type: "string", description: "Places only: comma-separated leaf categories, e.g. 'cafe,coffee_shop'" },
|
|
95
|
+
limit: { type: "integer", description: "Max features (default 20, cap #{MAX_FEATURES})" }
|
|
96
|
+
),
|
|
97
|
+
required: ["theme"]
|
|
98
|
+
)
|
|
99
|
+
|
|
100
|
+
def self.call(theme:, type: nil, category: nil, limit: nil, server_context: nil, **bbox_args)
|
|
101
|
+
bbox = Helpers.resolve_bbox(bbox_args)
|
|
102
|
+
limit = [(limit || 20).to_i.clamp(1, MAX_FEATURES), MAX_FEATURES].min
|
|
103
|
+
wanted = category&.split(",")&.map(&:strip)
|
|
104
|
+
|
|
105
|
+
features = []
|
|
106
|
+
Query.new(theme: theme, type: type, bbox: bbox, limit: limit * 5).each do |record|
|
|
107
|
+
if wanted
|
|
108
|
+
cats = record["categories"]
|
|
109
|
+
primary = cats.is_a?(Hash) ? cats["primary"] : record["basic_category"]
|
|
110
|
+
next unless wanted.include?(primary) ||
|
|
111
|
+
(cats.is_a?(Hash) && (Array(cats["alternate"]) & wanted).any?)
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
geometry = record.delete("geometry")
|
|
115
|
+
properties = record.except("bbox", "names", "sources")
|
|
116
|
+
properties["name"] = record.dig("names", "primary")
|
|
117
|
+
features << {
|
|
118
|
+
type: "Feature",
|
|
119
|
+
geometry: geometry && RGeo::GeoJSON.encode(geometry),
|
|
120
|
+
properties: properties
|
|
121
|
+
}
|
|
122
|
+
break if features.length >= limit
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
Helpers.text_response({ type: "FeatureCollection", features: features })
|
|
126
|
+
rescue OvertureMaps::Error, ArgumentError => e
|
|
127
|
+
Helpers.error_response(e.message)
|
|
128
|
+
end
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
class CountFeaturesTool < MCP::Tool
|
|
132
|
+
tool_name "count_features"
|
|
133
|
+
description "Count Overture Maps features of a theme in an area without downloading them."
|
|
134
|
+
input_schema(
|
|
135
|
+
properties: Helpers.bbox_properties.merge(
|
|
136
|
+
theme: { type: "string" },
|
|
137
|
+
type: { type: "string" }
|
|
138
|
+
),
|
|
139
|
+
required: ["theme"]
|
|
140
|
+
)
|
|
141
|
+
|
|
142
|
+
def self.call(theme:, type: nil, server_context: nil, **bbox_args)
|
|
143
|
+
bbox = Helpers.resolve_bbox(bbox_args)
|
|
144
|
+
count = Query.new(theme: theme, type: type, bbox: bbox).count
|
|
145
|
+
Helpers.text_response({ theme: theme, type: type, bbox: bbox.to_s, count: count })
|
|
146
|
+
rescue OvertureMaps::Error, ArgumentError => e
|
|
147
|
+
Helpers.error_response(e.message)
|
|
148
|
+
end
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
class ExportGeojsonTool < MCP::Tool
|
|
152
|
+
tool_name "export_geojson"
|
|
153
|
+
description "Export Overture Maps features for an area to a local file. Format comes from " \
|
|
154
|
+
"the extension: .geojson, .geojsonseq, .gpkg, or .parquet."
|
|
155
|
+
input_schema(
|
|
156
|
+
properties: Helpers.bbox_properties.merge(
|
|
157
|
+
theme: { type: "string" },
|
|
158
|
+
type: { type: "string" },
|
|
159
|
+
path: { type: "string", description: "Output file path" },
|
|
160
|
+
limit: { type: "integer" }
|
|
161
|
+
),
|
|
162
|
+
required: %w[theme path]
|
|
163
|
+
)
|
|
164
|
+
|
|
165
|
+
def self.call(theme:, path:, type: nil, limit: nil, server_context: nil, **bbox_args)
|
|
166
|
+
bbox = Helpers.resolve_bbox(bbox_args)
|
|
167
|
+
query = Query.new(theme: theme, type: type, bbox: bbox)
|
|
168
|
+
query = query.limit(limit) if limit
|
|
169
|
+
output = query.export(File.expand_path(path))
|
|
170
|
+
Helpers.text_response({ exported: output, bytes: File.size(output) })
|
|
171
|
+
rescue OvertureMaps::Error, ArgumentError => e
|
|
172
|
+
Helpers.error_response(e.message)
|
|
173
|
+
end
|
|
174
|
+
end
|
|
175
|
+
|
|
176
|
+
class GersLookupTool < MCP::Tool
|
|
177
|
+
tool_name "gers_lookup"
|
|
178
|
+
description "Look up a GERS id (Overture's stable entity id) in the official registry: " \
|
|
179
|
+
"when it first appeared, when it last changed, and where it lives."
|
|
180
|
+
input_schema(
|
|
181
|
+
properties: { id: { type: "string", description: "GERS id (dashed UUID)" } },
|
|
182
|
+
required: ["id"]
|
|
183
|
+
)
|
|
184
|
+
|
|
185
|
+
def self.call(id:, server_context: nil)
|
|
186
|
+
row = GERS.lookup(id)
|
|
187
|
+
Helpers.text_response(row || { error: "not found in the registry" })
|
|
188
|
+
rescue OvertureMaps::Error, ArgumentError => e
|
|
189
|
+
Helpers.error_response(e.message)
|
|
190
|
+
end
|
|
191
|
+
end
|
|
192
|
+
|
|
193
|
+
class ListReleasesTool < MCP::Tool
|
|
194
|
+
tool_name "list_releases"
|
|
195
|
+
description "List available Overture Maps data releases, newest first."
|
|
196
|
+
input_schema(properties: {}, required: [])
|
|
197
|
+
|
|
198
|
+
def self.call(server_context: nil)
|
|
199
|
+
Helpers.text_response({ releases: Releases.all, latest: Releases.latest })
|
|
200
|
+
rescue OvertureMaps::Error => e
|
|
201
|
+
Helpers.error_response(e.message)
|
|
202
|
+
end
|
|
203
|
+
end
|
|
204
|
+
|
|
205
|
+
TOOLS = [
|
|
206
|
+
GeocodeTool, QueryFeaturesTool, CountFeaturesTool,
|
|
207
|
+
ExportGeojsonTool, GersLookupTool, ListReleasesTool
|
|
208
|
+
].freeze
|
|
209
|
+
|
|
210
|
+
def self.start
|
|
211
|
+
server = MCP::Server.new(
|
|
212
|
+
name: "overture-maps",
|
|
213
|
+
version: OvertureMaps::VERSION,
|
|
214
|
+
instructions: "Query Overture Maps open geospatial data: geocode place names, fetch and " \
|
|
215
|
+
"count features (places, buildings, roads, ...), export GeoJSON, and look " \
|
|
216
|
+
"up GERS ids. Read-only; data streams from Overture's public bucket.",
|
|
217
|
+
tools: TOOLS
|
|
218
|
+
)
|
|
219
|
+
MCP::Server::Transports::StdioTransport.new(server).open
|
|
220
|
+
end
|
|
221
|
+
end
|
|
222
|
+
end
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module OvertureMaps
|
|
4
|
+
module Models
|
|
5
|
+
class Address < Base
|
|
6
|
+
self.table_name = "overture_addresses"
|
|
7
|
+
|
|
8
|
+
scope :by_country, ->(country) {
|
|
9
|
+
where(country: country)
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
scope :by_locality, ->(locality) {
|
|
13
|
+
where(locality: locality)
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
scope :by_region, ->(region) {
|
|
17
|
+
where(region: region)
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
scope :by_postcode, ->(postcode) {
|
|
21
|
+
where(postcode: postcode)
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
def full_address
|
|
25
|
+
[
|
|
26
|
+
[number, street].compact.join(" "),
|
|
27
|
+
unit, locality, region, postcode, country
|
|
28
|
+
].compact.reject(&:empty?).join(", ")
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "rgeo"
|
|
4
|
+
require "rgeo/geo_json"
|
|
5
|
+
|
|
6
|
+
module OvertureMaps
|
|
7
|
+
module Models
|
|
8
|
+
class Base < ::ActiveRecord::Base
|
|
9
|
+
self.abstract_class = true
|
|
10
|
+
|
|
11
|
+
# Everything whose geometry intersects the bounding box. Works for any
|
|
12
|
+
# geometry type (points, building polygons, segments).
|
|
13
|
+
scope :within_bounds, ->(south, west, north, east) {
|
|
14
|
+
where(
|
|
15
|
+
"ST_Intersects(geometry, ST_MakeEnvelope(?, ?, ?, ?, 4326)::geography)",
|
|
16
|
+
west, south, east, north
|
|
17
|
+
)
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
# Within radius_meters of a point.
|
|
21
|
+
scope :near, ->(lat, lng, radius_meters = 1000) {
|
|
22
|
+
where(
|
|
23
|
+
"ST_DWithin(geometry, ST_SetSRID(ST_MakePoint(?, ?), 4326)::geography, ?)",
|
|
24
|
+
lng, lat, radius_meters
|
|
25
|
+
)
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
def to_geojson
|
|
29
|
+
return nil unless geometry
|
|
30
|
+
|
|
31
|
+
{
|
|
32
|
+
type: "Feature",
|
|
33
|
+
geometry: RGeo::GeoJSON.encode(geometry),
|
|
34
|
+
properties: attributes.except("geometry", "created_at", "updated_at")
|
|
35
|
+
}
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module OvertureMaps
|
|
4
|
+
module Models
|
|
5
|
+
# All six base-theme feature types (bathymetry, infrastructure, land,
|
|
6
|
+
# land_cover, land_use, water) in one table, discriminated by
|
|
7
|
+
# feature_type.
|
|
8
|
+
class BaseFeature < Base
|
|
9
|
+
self.table_name = "overture_base_features"
|
|
10
|
+
|
|
11
|
+
scope :by_feature_type, ->(feature_type) {
|
|
12
|
+
where(feature_type: feature_type)
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
scope :by_subtype, ->(subtype) {
|
|
16
|
+
where(subtype: subtype)
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
scope :by_class, ->(feature_class) {
|
|
20
|
+
where(feature_class: feature_class)
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
scope :water, -> { by_feature_type("water") }
|
|
24
|
+
scope :land, -> { by_feature_type("land") }
|
|
25
|
+
scope :land_use, -> { by_feature_type("land_use") }
|
|
26
|
+
scope :land_cover, -> { by_feature_type("land_cover") }
|
|
27
|
+
scope :infrastructure, -> { by_feature_type("infrastructure") }
|
|
28
|
+
scope :bathymetry, -> { by_feature_type("bathymetry") }
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module OvertureMaps
|
|
4
|
+
module Models
|
|
5
|
+
class Building < Base
|
|
6
|
+
self.table_name = "overture_buildings"
|
|
7
|
+
|
|
8
|
+
scope :by_height, ->(min: nil, max: nil) {
|
|
9
|
+
query = all
|
|
10
|
+
query = query.where("height >= ?", min) if min
|
|
11
|
+
query = query.where("height <= ?", max) if max
|
|
12
|
+
query
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
scope :by_floors, ->(min: nil, max: nil) {
|
|
16
|
+
query = all
|
|
17
|
+
query = query.where("num_floors >= ?", min) if min
|
|
18
|
+
query = query.where("num_floors <= ?", max) if max
|
|
19
|
+
query
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
scope :by_class, ->(building_class) {
|
|
23
|
+
where(building_class: building_class)
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
scope :with_height, -> {
|
|
27
|
+
where("height IS NOT NULL AND height > 0")
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
scope :underground, -> {
|
|
31
|
+
where(is_underground: true)
|
|
32
|
+
}
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module OvertureMaps
|
|
4
|
+
module Models
|
|
5
|
+
# Overture places category taxonomy (no geometry — plain lookup table).
|
|
6
|
+
class Category < ::ActiveRecord::Base
|
|
7
|
+
self.table_name = "overture_categories"
|
|
8
|
+
|
|
9
|
+
validates :name, presence: true, uniqueness: true
|
|
10
|
+
|
|
11
|
+
scope :by_primary, ->(primary) {
|
|
12
|
+
where(primary_category: primary)
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
def self.primary_categories
|
|
16
|
+
distinct.where.not(primary_category: nil).pluck(:primary_category).sort
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
# Expands taxonomy groups to the leaf categories beneath them, at any
|
|
20
|
+
# depth ("eat_and_drink" or "restaurant" → every *_restaurant leaf).
|
|
21
|
+
# Names that aren't groups pass through unchanged, so mixed input like
|
|
22
|
+
# ["eat_and_drink", "museum"] works.
|
|
23
|
+
def self.expand(names)
|
|
24
|
+
names = Array(names).map(&:to_s)
|
|
25
|
+
leaves = names.flat_map do |group|
|
|
26
|
+
where("taxonomy @> ?", [group].to_json).pluck(:name)
|
|
27
|
+
end
|
|
28
|
+
(names + leaves).uniq
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module OvertureMaps
|
|
4
|
+
module Models
|
|
5
|
+
# Imported division areas (theme=divisions/type=division_area): the
|
|
6
|
+
# geocodable territories of countries, regions, counties, localities, etc.
|
|
7
|
+
# Once populated, location searches resolve locally instead of querying
|
|
8
|
+
# Overture's bucket.
|
|
9
|
+
class Division < Base
|
|
10
|
+
self.table_name = "overture_divisions"
|
|
11
|
+
|
|
12
|
+
scope :by_subtype, ->(subtype) {
|
|
13
|
+
where(subtype: subtype)
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
scope :by_country, ->(country) {
|
|
17
|
+
where(country: country)
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
scope :countries, -> {
|
|
21
|
+
where(subtype: "country")
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
scope :search_by_name, ->(query) {
|
|
25
|
+
where("name ILIKE ?", "%#{sanitize_sql_like(query)}%")
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
scope :largest_first, -> {
|
|
29
|
+
order(Arel.sql("(bbox_xmax - bbox_xmin) * (bbox_ymax - bbox_ymin) DESC"))
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
def to_bounding_box
|
|
33
|
+
return nil unless bbox_xmin && bbox_xmax && bbox_ymin && bbox_ymax
|
|
34
|
+
|
|
35
|
+
BoundingBox.new(
|
|
36
|
+
lat1: bbox_ymin, lng1: bbox_xmin,
|
|
37
|
+
lat2: bbox_ymax, lng2: bbox_xmax,
|
|
38
|
+
display_name: name
|
|
39
|
+
)
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
end
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module OvertureMaps
|
|
4
|
+
module Models
|
|
5
|
+
# Bookkeeping for every area/type combination that has been imported:
|
|
6
|
+
# which release the rows came from and the bbox they cover. This is what
|
|
7
|
+
# overture_maps:sync iterates to bring tables up to a newer release.
|
|
8
|
+
class ImportedArea < ::ActiveRecord::Base
|
|
9
|
+
self.table_name = "overture_imported_areas"
|
|
10
|
+
|
|
11
|
+
validates :theme, :feature_type, :model_class_name, :slug, :release, presence: true
|
|
12
|
+
|
|
13
|
+
scope :for_release, ->(release) { where(release: release) }
|
|
14
|
+
scope :behind, ->(release) { where.not(release: release) }
|
|
15
|
+
|
|
16
|
+
def to_bounding_box
|
|
17
|
+
BoundingBox.new(
|
|
18
|
+
lat1: bbox_ymin, lng1: bbox_xmin,
|
|
19
|
+
lat2: bbox_ymax, lng2: bbox_xmax,
|
|
20
|
+
display_name: slug
|
|
21
|
+
)
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def model_class
|
|
25
|
+
model_class_name.constantize
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
# Upserts the bookkeeping row after an import.
|
|
29
|
+
def self.record!(theme:, feature_type:, model_class:, bbox:, release:, records_count:)
|
|
30
|
+
area = find_or_initialize_by(theme: theme, feature_type: feature_type, slug: bbox.slug)
|
|
31
|
+
area.update!(
|
|
32
|
+
model_class_name: model_class.name,
|
|
33
|
+
bbox_xmin: bbox.min_lng, bbox_xmax: bbox.max_lng,
|
|
34
|
+
bbox_ymin: bbox.min_lat, bbox_ymax: bbox.max_lat,
|
|
35
|
+
release: release,
|
|
36
|
+
records_count: records_count
|
|
37
|
+
)
|
|
38
|
+
area
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module OvertureMaps
|
|
4
|
+
module Models
|
|
5
|
+
class Place < Base
|
|
6
|
+
self.table_name = "overture_places"
|
|
7
|
+
|
|
8
|
+
# Matches the primary category or any alternate. Takes leaf category
|
|
9
|
+
# names from the Overture taxonomy (e.g. "cafe", "restaurant").
|
|
10
|
+
scope :by_category, ->(categories) {
|
|
11
|
+
cats = Array(categories).map(&:to_s)
|
|
12
|
+
where(
|
|
13
|
+
"primary_category = ANY (ARRAY[:cats]::text[]) OR categories->'alternate' ?| ARRAY[:cats]::text[]",
|
|
14
|
+
cats: cats
|
|
15
|
+
)
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
scope :by_country, ->(country) {
|
|
19
|
+
where(country: country)
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
# Matches the brand's primary name, e.g. by_brand("Starbucks").
|
|
23
|
+
scope :by_brand, ->(brands) {
|
|
24
|
+
where(
|
|
25
|
+
"brands->'names'->>'primary' = ANY (ARRAY[:brands]::text[])",
|
|
26
|
+
brands: Array(brands).map(&:to_s)
|
|
27
|
+
)
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
scope :by_operating_status, ->(status) {
|
|
31
|
+
where(operating_status: status)
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
scope :min_confidence, ->(confidence) {
|
|
35
|
+
where("confidence >= ?", confidence)
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
def brand_name
|
|
39
|
+
brands&.dig("names", "primary")
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
end
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module OvertureMaps
|
|
4
|
+
module Models
|
|
5
|
+
# Transportation segments: center-lines of roads, rails, and waterways.
|
|
6
|
+
class Segment < Base
|
|
7
|
+
self.table_name = "overture_segments"
|
|
8
|
+
|
|
9
|
+
scope :by_subtype, ->(subtype) {
|
|
10
|
+
where(subtype: subtype)
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
scope :by_class, ->(segment_class) {
|
|
14
|
+
where(segment_class: segment_class)
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
scope :roads, -> { where(subtype: "road") }
|
|
18
|
+
scope :rails, -> { where(subtype: "rail") }
|
|
19
|
+
scope :waterways, -> { where(subtype: "water") }
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# This gem is an ActiveRecord integration at heart, so the model layer loads
|
|
4
|
+
# eagerly. (Deferring via on_load(:active_record) breaks rake tasks: Zeitwerk
|
|
5
|
+
# can autoload an app's OverturePlace subclass before anything has touched
|
|
6
|
+
# ActiveRecord::Base, at which point the gem superclass wouldn't exist yet.)
|
|
7
|
+
require "active_record"
|
|
8
|
+
require "rgeo/active_record"
|
|
9
|
+
require "activerecord-postgis-adapter"
|
|
10
|
+
|
|
11
|
+
module OvertureMaps
|
|
12
|
+
module Models
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
require "overture_maps/models/base"
|
|
17
|
+
require "overture_maps/models/place"
|
|
18
|
+
require "overture_maps/models/building"
|
|
19
|
+
require "overture_maps/models/address"
|
|
20
|
+
require "overture_maps/models/category"
|
|
21
|
+
require "overture_maps/models/division"
|
|
22
|
+
require "overture_maps/models/segment"
|
|
23
|
+
require "overture_maps/models/connector"
|
|
24
|
+
require "overture_maps/models/base_feature"
|
|
25
|
+
require "overture_maps/models/imported_area"
|