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
@@ -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,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ module OvertureMaps
4
+ module Models
5
+ # Transportation connectors: points where segments physically meet,
6
+ # usable as routing nodes.
7
+ class Connector < Base
8
+ self.table_name = "overture_connectors"
9
+ end
10
+ end
11
+ 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"