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,554 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "overture_maps"
4
+
5
+ module OvertureMaps
6
+ # Interactive glue for the rake tasks. Prompts and process exits live here
7
+ # and only here — the library classes raise or take callbacks instead.
8
+ module RakeUI
9
+ # Which feature types are imported per theme, and into which model.
10
+ # building_part is skipped for now (needs a building_id relationship).
11
+ IMPORT_THEMES = {
12
+ "places" => { "place" => "OverturePlace" },
13
+ "buildings" => { "building" => "OvertureBuilding" },
14
+ "addresses" => { "address" => "OvertureAddress" },
15
+ "divisions" => { "division_area" => "OvertureDivision" },
16
+ "transportation" => {
17
+ "segment" => "OvertureSegment",
18
+ "connector" => "OvertureConnector"
19
+ },
20
+ "base" => OvertureMaps::Import::Downloader::TYPES["base"]
21
+ .to_h { |t| [t, "OvertureBaseFeature"] }
22
+ }.freeze
23
+
24
+ module_function
25
+
26
+ def interactive?
27
+ !OvertureMaps.configuration.non_interactive && $stdin.tty?
28
+ end
29
+
30
+ def abort!(message)
31
+ puts message
32
+ exit 1
33
+ end
34
+
35
+ # Resolves { type => model_name } to { type => model_class }, verifying
36
+ # tables exist.
37
+ def require_models!(theme)
38
+ mapping = IMPORT_THEMES[theme] or abort!("No import models for theme #{theme}")
39
+ mapping.to_h do |type, name|
40
+ model = begin
41
+ name.constantize
42
+ rescue NameError
43
+ abort!("#{name} model not found. Run: rails generate overture_maps:install")
44
+ end
45
+ abort!("#{name} table does not exist. Run: rails db:migrate") unless model.table_exists?
46
+ [type, model]
47
+ end
48
+ end
49
+
50
+ def select_division_callback
51
+ return nil unless interactive?
52
+
53
+ ->(results) { prompt_division(results) }
54
+ end
55
+
56
+ def confirm_cached_callback
57
+ return nil unless interactive?
58
+
59
+ ->(path) { prompt_cached(path) }
60
+ end
61
+
62
+ def prompt_division(results)
63
+ puts "Multiple matches found:"
64
+ results.each_with_index do |r, i|
65
+ location_info = [r[:country], r[:region]].compact.join(" / ")
66
+ area_info = r[:area_km2]&.positive? ? " (#{r[:area_km2]} km²)" : ""
67
+ puts " #{i + 1}. #{r[:name]} (#{r[:subtype]}) - #{location_info}#{area_info}"
68
+ end
69
+ puts
70
+ print "Enter number to select (or 'q' to quit): "
71
+ input = $stdin.gets&.strip
72
+
73
+ return nil if input.nil? || input.downcase == "q"
74
+
75
+ index = input.to_i - 1
76
+ return nil unless index >= 0 && index < results.length
77
+
78
+ results[index]
79
+ end
80
+
81
+ def prompt_cached(path)
82
+ size = OvertureMaps::Util.format_size(File.size(path))
83
+ puts "Found cached extract: #{path} (#{size})"
84
+ print "Use it? (y = use / d = download fresh / q = quit): "
85
+
86
+ case $stdin.gets&.strip&.downcase
87
+ when "y", "yes", "", nil then :use
88
+ when "d", "download" then :refresh
89
+ else :abort
90
+ end
91
+ end
92
+
93
+ # Resolves a location argument (bbox string or division name) to a
94
+ # BoundingBox for the download tasks.
95
+ def resolve_bbox(location, release: nil)
96
+ bbox = OvertureMaps::BoundingBox.parse(location)
97
+ return bbox if bbox
98
+
99
+ results = OvertureMaps::DivisionSearch.search(query: location, release: release)
100
+ abort!("No divisions found matching '#{location}'") if results.empty?
101
+
102
+ division = results.length == 1 || !interactive? ? results.first : prompt_division(results)
103
+ if division.nil?
104
+ puts "Cancelled."
105
+ exit 0
106
+ end
107
+
108
+ info = [division[:country], division[:region]].compact.join(" / ")
109
+ puts "Using #{division[:name]} (#{division[:subtype]}#{info.empty? ? "" : ", #{info}"})"
110
+ division[:bbox]
111
+ end
112
+
113
+ def print_divisions(results)
114
+ results.each_with_index do |r, i|
115
+ location_parts = [r[:country], r[:region]].compact
116
+ location = location_parts.any? ? " - #{location_parts.join(" / ")}" : ""
117
+ area_info = r[:area_km2]&.positive? ? " [#{r[:area_km2]} km²]" : ""
118
+ puts " #{i + 1}. #{r[:name]} (#{r[:subtype]})#{location}#{area_info}"
119
+ end
120
+ end
121
+
122
+ def release_arg
123
+ ENV["OVERTURE_RELEASE"]
124
+ end
125
+ end
126
+ end
127
+
128
+ namespace :overture_maps do
129
+ namespace :import do
130
+ OvertureMaps::RakeUI::IMPORT_THEMES.each_key do |theme|
131
+ desc "Import #{theme} by location name or bounding box, e.g. rails overture_maps:import:#{theme}[Seattle]"
132
+ task theme.to_sym, [:location, :categories] => :environment do |_t, args|
133
+ location = args[:location]
134
+ unless location
135
+ puts "Usage:"
136
+ puts " rails overture_maps:import:#{theme}[Seattle]"
137
+ puts " rails \"overture_maps:import:#{theme}[47.606_-122.336_47.609_-122.333]\""
138
+ puts " rails overture_maps:import:#{theme}[Seattle,\"cafe,restaurant\"] # categories or groups" if theme == "places"
139
+ exit 1
140
+ end
141
+
142
+ categories = args[:categories]&.split(",")&.map(&:strip)
143
+ models = OvertureMaps::RakeUI.require_models!(theme)
144
+
145
+ begin
146
+ runner = OvertureMaps::Import::LocationBasedRunner.new(
147
+ theme: theme,
148
+ location: location,
149
+ models: models,
150
+ categories: categories,
151
+ release: OvertureMaps::RakeUI.release_arg,
152
+ select_division: OvertureMaps::RakeUI.select_division_callback,
153
+ confirm_cached: OvertureMaps::RakeUI.confirm_cached_callback
154
+ ).run
155
+ rescue OvertureMaps::CancelledError
156
+ puts "Cancelled."
157
+ exit 0
158
+ rescue OvertureMaps::Error => e
159
+ OvertureMaps::RakeUI.abort!("Error: #{e.message}")
160
+ end
161
+
162
+ puts
163
+ puts "Import complete: #{runner.imported_count} imported, #{runner.error_count} errors"
164
+ if runner.errors.any? && ENV["VERBOSE"]
165
+ runner.errors.first(10).each { |err| puts " - #{err[:error]} (#{err[:record_id]})" }
166
+ end
167
+ exit 1 if runner.error_count.positive? && !ENV["IGNORE_ERRORS"]
168
+ end
169
+ end
170
+
171
+ desc "Import all themes for a location"
172
+ task :all, [:location] => :environment do |_t, args|
173
+ location = args[:location] or OvertureMaps::RakeUI.abort!(
174
+ "Usage: rails overture_maps:import:all[Seattle]"
175
+ )
176
+
177
+ # Resolve the location once so each theme reuses the same bbox instead
178
+ # of re-searching (and re-prompting).
179
+ bbox = OvertureMaps::RakeUI.resolve_bbox(location, release: OvertureMaps::RakeUI.release_arg)
180
+
181
+ failures = 0
182
+ OvertureMaps::RakeUI::IMPORT_THEMES.each_key do |theme|
183
+ puts "=" * 60
184
+ puts "Importing #{theme}..."
185
+ puts "=" * 60
186
+
187
+ models = OvertureMaps::RakeUI.require_models!(theme)
188
+ runner = OvertureMaps::Import::LocationBasedRunner.new(
189
+ theme: theme, location: bbox, models: models,
190
+ release: OvertureMaps::RakeUI.release_arg,
191
+ confirm_cached: OvertureMaps::RakeUI.confirm_cached_callback
192
+ ).run
193
+ puts "#{theme}: #{runner.imported_count} imported, #{runner.error_count} errors"
194
+ failures += runner.error_count
195
+ puts
196
+ end
197
+
198
+ puts "All imports complete!"
199
+ exit 1 if failures.positive? && !ENV["IGNORE_ERRORS"]
200
+ end
201
+
202
+ desc "Search for geographic divisions"
203
+ task :search, [:query] => :environment do |_t, args|
204
+ query = args[:query] or OvertureMaps::RakeUI.abort!(
205
+ "Usage: rails overture_maps:import:search[Seattle]"
206
+ )
207
+
208
+ results = OvertureMaps::DivisionSearch.search(
209
+ query: query, release: OvertureMaps::RakeUI.release_arg
210
+ )
211
+ if results.empty?
212
+ puts "No divisions found matching '#{query}'"
213
+ else
214
+ puts "Results:"
215
+ OvertureMaps::RakeUI.print_divisions(results)
216
+ puts
217
+ puts "Tip: rails overture_maps:import:places[#{results.first[:name]}]"
218
+ end
219
+ rescue OvertureMaps::Error => e
220
+ OvertureMaps::RakeUI.abort!("Error: #{e.message}")
221
+ end
222
+
223
+ desc "Show import statistics"
224
+ task stats: :environment do
225
+ OvertureMaps::RakeUI::IMPORT_THEMES.values.flat_map(&:values).uniq.each do |model_name|
226
+ count = begin
227
+ model_name.constantize.count
228
+ rescue StandardError
229
+ "N/A (run rails generate overture_maps:install && rails db:migrate)"
230
+ end
231
+ puts format(" %-24s %s", "#{model_name}:", count)
232
+ end
233
+ end
234
+ end
235
+
236
+ namespace :download do
237
+ OvertureMaps::Import::Downloader::THEMES.each do |theme|
238
+ desc "Download #{theme} data by location name or bbox; without a location downloads complete theme files (large!)"
239
+ task theme.to_sym, [:location, :release, :output_dir] do |_t, args|
240
+ release = args[:release] || OvertureMaps::RakeUI.release_arg
241
+ downloader = OvertureMaps::Import::Downloader.new(
242
+ theme: theme, release: release, output_dir: args[:output_dir]
243
+ )
244
+
245
+ begin
246
+ if args[:location]
247
+ bbox = OvertureMaps::RakeUI.resolve_bbox(args[:location], release: release)
248
+ files = downloader.extract_bbox_all_types(bbox)
249
+ puts "\nDownloaded #{files.count} extract(s)"
250
+ else
251
+ puts "No location given — downloading ALL #{theme} files for #{downloader.release}."
252
+ puts "This can be very large; prefer overture_maps:download:#{theme}[<location>]."
253
+ count = downloader.download_theme_files
254
+ puts "\nDownloaded #{count} file(s)"
255
+ end
256
+ rescue OvertureMaps::Error => e
257
+ OvertureMaps::RakeUI.abort!("Error: #{e.message}")
258
+ end
259
+ end
260
+ end
261
+
262
+ desc "Download data within a bounding box, e.g. rails overture_maps:download:bbox[places,47.6,-122.4,47.7,-122.2]"
263
+ task :bbox, [:theme, :lat1, :lng1, :lat2, :lng2, :type, :release, :output_dir, :format] do |_t, args|
264
+ unless args[:lat1] && args[:lng1] && args[:lat2] && args[:lng2]
265
+ OvertureMaps::RakeUI.abort!("Usage: rails overture_maps:download:bbox[theme,lat1,lng1,lat2,lng2]")
266
+ end
267
+
268
+ bbox = OvertureMaps::BoundingBox.new(
269
+ lat1: args[:lat1], lng1: args[:lng1], lat2: args[:lat2], lng2: args[:lng2]
270
+ )
271
+ downloader = OvertureMaps::Import::Downloader.new(
272
+ theme: args[:theme] || "places", type: args[:type],
273
+ release: args[:release] || OvertureMaps::RakeUI.release_arg,
274
+ output_dir: args[:output_dir]
275
+ )
276
+ files = downloader.extract_bbox_all_types(bbox, format: args[:format] || "parquet")
277
+ puts "\nDownloaded #{files.count} extract(s)"
278
+ rescue OvertureMaps::Error, ArgumentError => e
279
+ OvertureMaps::RakeUI.abort!("Error: #{e.message}")
280
+ end
281
+
282
+ desc "Download data near a point, e.g. rails overture_maps:download:nearby[places,47.6,-122.3,5000]"
283
+ task :nearby, [:theme, :lat, :lng, :radius, :type, :release, :output_dir] do |_t, args|
284
+ unless args[:lat] && args[:lng]
285
+ OvertureMaps::RakeUI.abort!("Usage: rails overture_maps:download:nearby[theme,lat,lng,radius_meters]")
286
+ end
287
+
288
+ downloader = OvertureMaps::Import::Downloader.new(
289
+ theme: args[:theme] || "places", type: args[:type],
290
+ release: args[:release] || OvertureMaps::RakeUI.release_arg,
291
+ output_dir: args[:output_dir]
292
+ )
293
+ files = downloader.extract_nearby(
294
+ lat: args[:lat], lng: args[:lng],
295
+ radius_meters: (args[:radius] || 5000).to_i
296
+ )
297
+ puts "\nDownloaded #{files.count} extract(s)"
298
+ rescue OvertureMaps::Error, ArgumentError => e
299
+ OvertureMaps::RakeUI.abort!("Error: #{e.message}")
300
+ end
301
+
302
+ desc "Search for geographic divisions"
303
+ task :search_divisions, [:query] do |_t, args|
304
+ query = args[:query] or OvertureMaps::RakeUI.abort!(
305
+ "Usage: rails overture_maps:download:search_divisions[query]"
306
+ )
307
+
308
+ results = OvertureMaps::Import::Downloader.search_divisions(
309
+ query: query, release: OvertureMaps::RakeUI.release_arg
310
+ )
311
+ if results.empty?
312
+ puts "No divisions found matching '#{query}'"
313
+ else
314
+ puts "Results:"
315
+ OvertureMaps::RakeUI.print_divisions(results)
316
+ end
317
+ rescue OvertureMaps::Error => e
318
+ OvertureMaps::RakeUI.abort!("Error: #{e.message}")
319
+ end
320
+
321
+ desc "List available Overture releases"
322
+ task :versions do
323
+ releases = OvertureMaps::Releases.all
324
+ puts "Available releases:"
325
+ releases.each_with_index do |release, i|
326
+ puts " - #{release}#{i.zero? ? " (latest)" : ""}"
327
+ end
328
+ rescue OvertureMaps::Error => e
329
+ OvertureMaps::RakeUI.abort!("Error: #{e.message}")
330
+ end
331
+
332
+ desc "List available themes and their types"
333
+ task :themes do
334
+ OvertureMaps::Import::Downloader.themes_with_types.each do |theme, types|
335
+ puts " - #{theme}"
336
+ types.each { |t| puts " #{t}" }
337
+ end
338
+ end
339
+
340
+ desc "List types available for a theme in the current release"
341
+ task :types, [:theme] do |_t, args|
342
+ theme = args[:theme] || "places"
343
+ types = OvertureMaps::Import::Downloader.list_types(
344
+ theme: theme, release: OvertureMaps::RakeUI.release_arg
345
+ )
346
+ puts "Available types for #{theme}:"
347
+ types.each { |t| puts " - #{t}" }
348
+ rescue OvertureMaps::Error => e
349
+ OvertureMaps::RakeUI.abort!("Error: #{e.message}")
350
+ end
351
+
352
+ desc "List files for a theme/type without downloading"
353
+ task :list, [:theme, :type, :release] do |_t, args|
354
+ downloader = OvertureMaps::Import::Downloader.new(
355
+ theme: args[:theme] || "places", type: args[:type],
356
+ release: args[:release] || OvertureMaps::RakeUI.release_arg
357
+ )
358
+ files = downloader.list_files
359
+ if files.empty?
360
+ puts "No files found"
361
+ else
362
+ files.each do |f|
363
+ puts " #{File.basename(f[:key])} (#{OvertureMaps::Util.format_size(f[:size])})"
364
+ end
365
+ puts "Total: #{files.count} file(s)"
366
+ end
367
+ rescue OvertureMaps::Error => e
368
+ OvertureMaps::RakeUI.abort!("Error: #{e.message}")
369
+ end
370
+ end
371
+
372
+ desc "Sync imported areas to the latest (or given) Overture release"
373
+ task :sync, [:release] => :environment do |_t, args|
374
+ unless OvertureMaps::Models::ImportedArea.table_exists?
375
+ OvertureMaps::RakeUI.abort!(
376
+ "No sync tracking table. Run: rails generate overture_maps:install && rails db:migrate\n" \
377
+ "(Areas are tracked from their next import.)"
378
+ )
379
+ end
380
+
381
+ syncer = OvertureMaps::Syncer.new(target_release: args[:release])
382
+ areas = OvertureMaps::Models::ImportedArea.count
383
+ if areas.zero?
384
+ puts "No imported areas tracked yet. Run an import first."
385
+ next
386
+ end
387
+
388
+ puts "Syncing #{areas} imported area(s) to #{syncer.target}..."
389
+ puts
390
+
391
+ failures = 0
392
+ syncer.sync_all.each do |result|
393
+ area = result.area
394
+ label = "#{area.theme}/#{area.feature_type} #{area.slug}"
395
+ case result.status
396
+ when :up_to_date
397
+ puts " #{label}: already at #{syncer.target}"
398
+ when :synced
399
+ puts " #{label}: synced (removed #{result.removed}, upserted #{result.imported}, errors #{result.errors})"
400
+ when :refreshed
401
+ puts " #{label}: fully refreshed (#{result.message})"
402
+ when :failed
403
+ puts " #{label}: FAILED — #{result.message}"
404
+ failures += 1
405
+ end
406
+ failures += 1 if result.errors.positive? && result.status != :failed
407
+ end
408
+
409
+ puts
410
+ puts failures.zero? ? "Sync complete." : "Sync finished with #{failures} failure(s)."
411
+ exit 1 if failures.positive? && !ENV["IGNORE_ERRORS"]
412
+ end
413
+
414
+ namespace :sync do
415
+ desc "Show tracked areas and their releases"
416
+ task status: :environment do
417
+ unless OvertureMaps::Models::ImportedArea.table_exists?
418
+ OvertureMaps::RakeUI.abort!("No sync tracking table. Run: rails generate overture_maps:install && rails db:migrate")
419
+ end
420
+
421
+ latest = OvertureMaps::Releases.latest
422
+ puts "Latest Overture release: #{latest}"
423
+ puts
424
+
425
+ areas = OvertureMaps::Models::ImportedArea.order(:theme, :feature_type, :slug)
426
+ if areas.none?
427
+ puts "No imported areas tracked yet."
428
+ else
429
+ areas.each do |area|
430
+ marker = area.release == latest ? " " : "! "
431
+ puts "#{marker}#{area.theme}/#{area.feature_type} #{area.slug}: " \
432
+ "#{area.release} (#{area.records_count} records)"
433
+ end
434
+ behind = areas.count { |a| a.release != latest }
435
+ puts
436
+ puts behind.zero? ? "All areas up to date." : "#{behind} area(s) behind — run rails overture_maps:sync"
437
+ end
438
+ end
439
+ end
440
+
441
+ namespace :gers do
442
+ desc "Look up a GERS id in the Overture registry"
443
+ task :lookup, [:id] do |_t, args|
444
+ id = args[:id] or OvertureMaps::RakeUI.abort!("Usage: rails overture_maps:gers:lookup[<gers-id>]")
445
+
446
+ begin
447
+ row = OvertureMaps::GERS.lookup(id)
448
+ if row
449
+ row.each { |key, value| puts format(" %-14s %s", "#{key}:", value) }
450
+ else
451
+ puts "Not found in the registry."
452
+ end
453
+ rescue ArgumentError => e
454
+ OvertureMaps::RakeUI.abort!("Error: #{e.message}")
455
+ rescue OvertureMaps::Error => e
456
+ OvertureMaps::RakeUI.abort!("Error: #{e.message}")
457
+ end
458
+ end
459
+ end
460
+
461
+ namespace :cache do
462
+ desc "List cached extracts"
463
+ task :list do
464
+ dir = OvertureMaps.configuration.cache_dir
465
+ files = Dir.glob(File.join(dir, "*.{parquet,geojson,geojsonseq,gpkg}")).sort
466
+
467
+ if files.empty?
468
+ puts "No cached extracts in #{dir}"
469
+ else
470
+ total = 0
471
+ files.each do |f|
472
+ size = File.size(f)
473
+ total += size
474
+ puts " #{File.basename(f)} (#{OvertureMaps::Util.format_size(size)})"
475
+ end
476
+ puts "Total: #{files.count} file(s), #{OvertureMaps::Util.format_size(total)}"
477
+ end
478
+ end
479
+
480
+ desc "Remove cached extracts (optionally matching a pattern)"
481
+ task :clear, [:pattern] do |_t, args|
482
+ dir = OvertureMaps.configuration.cache_dir
483
+ pattern = args[:pattern] ? "*#{args[:pattern]}*" : "*"
484
+ files = Dir.glob(File.join(dir, "#{pattern}.{parquet,geojson,geojsonseq,gpkg}"))
485
+
486
+ if files.empty?
487
+ puts "Nothing to remove"
488
+ else
489
+ files.each { |f| File.delete(f) }
490
+ puts "Removed #{files.count} file(s)"
491
+ end
492
+ end
493
+ end
494
+
495
+ namespace :categories do
496
+ CATEGORIES_CSV_URL = "https://raw.githubusercontent.com/OvertureMaps/schema/main/docs/schema/concepts/by-theme/places/overture_categories.csv"
497
+
498
+ desc "Populate the categories taxonomy from the Overture schema repo"
499
+ task populate: :environment do
500
+ require "csv"
501
+
502
+ begin
503
+ OvertureCategory
504
+ rescue NameError
505
+ OvertureMaps::RakeUI.abort!("OvertureCategory model not found. Run: rails generate overture_maps:install")
506
+ end
507
+
508
+ puts "Fetching categories from the Overture Maps taxonomy..."
509
+ body = OvertureMaps::Storage.get(CATEGORIES_CSV_URL)
510
+
511
+ count = 0
512
+ skipped = 0
513
+ CSV.parse(body, headers: true, col_sep: ";").each do |row|
514
+ name = row[0]&.strip
515
+ taxonomy_str = row[1]&.strip
516
+
517
+ if name.nil? || name.empty? || taxonomy_str.nil? || taxonomy_str.empty?
518
+ skipped += 1 unless name.nil? && taxonomy_str.nil?
519
+ next
520
+ end
521
+
522
+ taxonomy = taxonomy_str.gsub(/[\[\]]/, "").split(",").map(&:strip)
523
+ category = OvertureCategory.find_or_initialize_by(name: name)
524
+ category.update!(
525
+ primary_category: taxonomy.first,
526
+ hierarchy_level: taxonomy.size - 1,
527
+ taxonomy: taxonomy
528
+ )
529
+ count += 1
530
+ end
531
+
532
+ puts "Imported #{count} categories#{skipped.positive? ? " (skipped #{skipped} malformed rows)" : ""}."
533
+ puts "\nPrimary categories:"
534
+ OvertureCategory.primary_categories.each { |pc| puts " - #{pc}" }
535
+ rescue OvertureMaps::Error => e
536
+ OvertureMaps::RakeUI.abort!("Error: #{e.message}")
537
+ end
538
+
539
+ desc "List all categories grouped by primary category"
540
+ task list: :environment do
541
+ OvertureCategory.primary_categories.each do |pc|
542
+ puts " - #{pc}"
543
+ OvertureCategory.by_primary(pc).order(:hierarchy_level, :name).each do |cat|
544
+ puts " #{cat.name}"
545
+ end
546
+ end
547
+ end
548
+
549
+ desc "List primary categories only"
550
+ task primary: :environment do
551
+ OvertureCategory.primary_categories.each { |pc| puts pc }
552
+ end
553
+ end
554
+ end