vehicles 0.6.1 → 0.8.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 (186) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop_todo.yml +164 -0
  3. data/CHANGELOG.md +274 -0
  4. data/README.md +55 -10
  5. data/data/PROVENANCE-vehicles.md +12 -0
  6. data/data/plates/PROVENANCE.md +5 -4
  7. data/data/plates/_decode/at-districts.yml +287 -0
  8. data/data/plates/_decode/ch-cantons.yml +110 -0
  9. data/data/plates/_decode/cn-issuing-offices.yml +463 -0
  10. data/data/plates/_decode/cn-provinces.yml +614 -0
  11. data/data/plates/_decode/cz-kraj.yml +159 -0
  12. data/data/plates/_decode/dk-serial-blocks.yml +327 -0
  13. data/data/plates/_decode/fr-departements.yml +191 -0
  14. data/data/plates/_decode/gb-age-identifiers.yml +170 -0
  15. data/data/plates/_decode/gb-memory-tags.yml +202 -0
  16. data/data/plates/_decode/gr-prefectures.yml +1408 -0
  17. data/data/plates/_decode/hr-cities.yml +315 -0
  18. data/data/plates/_decode/ie-counties.yml +227 -0
  19. data/data/plates/_decode/in-state-codes.yml +169 -0
  20. data/data/plates/_decode/is-districts-1938.yml +116 -0
  21. data/data/plates/_decode/it-provinces.yml +348 -0
  22. data/data/plates/_decode/jp-office-marks.yml +960 -0
  23. data/data/plates/_decode/jp-use-marks.yml +205 -0
  24. data/data/plates/_decode/kr-authority-marks.yml +146 -0
  25. data/data/plates/_decode/kr-use-syllables.yml +194 -0
  26. data/data/plates/_decode/lu-series-order.yml +638 -0
  27. data/data/plates/_decode/mx-entity-series.yml +380 -0
  28. data/data/plates/_decode/my-index-marks.yml +271 -0
  29. data/data/plates/_decode/no-counties-1913.yml +148 -0
  30. data/data/plates/_decode/no-districts.yml +901 -0
  31. data/data/plates/_decode/pl-powiats.yml +567 -0
  32. data/data/plates/_decode/pt-trailer-services.yml +96 -0
  33. data/data/plates/_decode/ro-counties.yml +213 -0
  34. data/data/plates/_decode/se-diplomatic.yml +220 -0
  35. data/data/plates/_decode/si-regions.yml +123 -0
  36. data/data/plates/_decode/sk-districts.yml +314 -0
  37. data/data/plates/_decode/th-registration-elements.yml +208 -0
  38. data/data/plates/_decode/ua-regions.yml +393 -0
  39. data/data/plates/_decode/za-licence-marks.yml +215 -0
  40. data/data/plates/_meta/classes.yml +1 -0
  41. data/data/plates/_meta/separators.yml +102 -0
  42. data/data/plates/ad.yml +961 -0
  43. data/data/plates/ar.yml +762 -0
  44. data/data/plates/at.yml +1424 -0
  45. data/data/plates/au-act.yml +472 -0
  46. data/data/plates/au-nsw.yml +282 -0
  47. data/data/plates/au-nt.yml +431 -0
  48. data/data/plates/au-qld.yml +272 -0
  49. data/data/plates/au-sa.yml +248 -0
  50. data/data/plates/au-tas.yml +302 -0
  51. data/data/plates/au-vic.yml +215 -0
  52. data/data/plates/au-wa.yml +309 -0
  53. data/data/plates/be.yml +1840 -0
  54. data/data/plates/br.yml +1152 -0
  55. data/data/plates/ca-ab.yml +430 -0
  56. data/data/plates/ca-bc.yml +697 -0
  57. data/data/plates/ca-mb.yml +499 -0
  58. data/data/plates/ca-nb.yml +537 -0
  59. data/data/plates/ca-nl.yml +743 -0
  60. data/data/plates/ca-ns.yml +808 -0
  61. data/data/plates/ca-nt.yml +610 -0
  62. data/data/plates/ca-nu.yml +280 -0
  63. data/data/plates/ca-on.yml +881 -0
  64. data/data/plates/ca-pe.yml +592 -0
  65. data/data/plates/ca-qc.yml +1036 -0
  66. data/data/plates/ca-sk.yml +256 -0
  67. data/data/plates/ca-yt.yml +656 -0
  68. data/data/plates/ch.yml +1273 -0
  69. data/data/plates/cn.yml +1431 -0
  70. data/data/plates/cy.yml +688 -0
  71. data/data/plates/cz.yml +1179 -0
  72. data/data/plates/de.yml +553 -118
  73. data/data/plates/dk.yml +1647 -0
  74. data/data/plates/ee.yml +1275 -0
  75. data/data/plates/es.yml +228 -32
  76. data/data/plates/fi.yml +1271 -0
  77. data/data/plates/fr.yml +1265 -0
  78. data/data/plates/gb.yml +1188 -0
  79. data/data/plates/gr.yml +721 -0
  80. data/data/plates/hr.yml +860 -0
  81. data/data/plates/hu.yml +959 -0
  82. data/data/plates/ie.yml +775 -0
  83. data/data/plates/in.yml +1172 -0
  84. data/data/plates/is.yml +667 -0
  85. data/data/plates/it.yml +1317 -0
  86. data/data/plates/jp.yml +3650 -0
  87. data/data/plates/kr.yml +1375 -0
  88. data/data/plates/li.yml +1010 -0
  89. data/data/plates/lt.yml +879 -0
  90. data/data/plates/lu.yml +842 -0
  91. data/data/plates/lv.yml +924 -0
  92. data/data/plates/mc.yml +1024 -0
  93. data/data/plates/mt.yml +854 -0
  94. data/data/plates/mx.yml +1372 -0
  95. data/data/plates/my.yml +685 -0
  96. data/data/plates/nl.yml +44 -0
  97. data/data/plates/no.yml +1018 -0
  98. data/data/plates/nz.yml +1699 -0
  99. data/data/plates/pl.yml +1296 -0
  100. data/data/plates/pt.yml +847 -0
  101. data/data/plates/ro.yml +785 -0
  102. data/data/plates/se.yml +1091 -0
  103. data/data/plates/si.yml +1105 -0
  104. data/data/plates/sk.yml +1439 -0
  105. data/data/plates/sm.yml +1119 -0
  106. data/data/plates/th.yml +1150 -0
  107. data/data/plates/ua.yml +1672 -0
  108. data/data/plates/us-ak.yml +763 -0
  109. data/data/plates/us-al.yml +457 -0
  110. data/data/plates/us-ar.yml +427 -0
  111. data/data/plates/us-as.yml +454 -0
  112. data/data/plates/us-az.yml +430 -0
  113. data/data/plates/us-ca.yml +892 -0
  114. data/data/plates/us-co.yml +452 -0
  115. data/data/plates/us-ct.yml +427 -0
  116. data/data/plates/us-dc.yml +614 -0
  117. data/data/plates/us-de.yml +551 -0
  118. data/data/plates/us-fl.yml +319 -96
  119. data/data/plates/us-ga.yml +644 -0
  120. data/data/plates/us-gu.yml +658 -0
  121. data/data/plates/us-hi.yml +585 -0
  122. data/data/plates/us-ia.yml +438 -0
  123. data/data/plates/us-id.yml +531 -0
  124. data/data/plates/us-il.yml +516 -0
  125. data/data/plates/us-in.yml +324 -0
  126. data/data/plates/us-ks.yml +351 -0
  127. data/data/plates/us-ky.yml +770 -0
  128. data/data/plates/us-la.yml +568 -0
  129. data/data/plates/us-ma.yml +379 -0
  130. data/data/plates/us-md.yml +536 -0
  131. data/data/plates/us-me.yml +445 -0
  132. data/data/plates/us-mi.yml +408 -0
  133. data/data/plates/us-mn.yml +411 -0
  134. data/data/plates/us-mo.yml +356 -0
  135. data/data/plates/us-mp.yml +402 -0
  136. data/data/plates/us-ms.yml +432 -0
  137. data/data/plates/us-mt.yml +619 -0
  138. data/data/plates/us-nc.yml +602 -0
  139. data/data/plates/us-nd.yml +419 -0
  140. data/data/plates/us-ne.yml +483 -0
  141. data/data/plates/us-nh.yml +394 -0
  142. data/data/plates/us-nj.yml +547 -0
  143. data/data/plates/us-nm.yml +574 -0
  144. data/data/plates/us-nv.yml +655 -0
  145. data/data/plates/us-ny.yml +592 -0
  146. data/data/plates/us-oh.yml +404 -0
  147. data/data/plates/us-ok.yml +513 -0
  148. data/data/plates/us-or.yml +392 -0
  149. data/data/plates/us-pa.yml +506 -0
  150. data/data/plates/us-pr.yml +507 -0
  151. data/data/plates/us-ri.yml +445 -0
  152. data/data/plates/us-sc.yml +605 -0
  153. data/data/plates/us-sd.yml +428 -0
  154. data/data/plates/us-tn.yml +471 -0
  155. data/data/plates/us-tx.yml +535 -0
  156. data/data/plates/us-ut.yml +384 -0
  157. data/data/plates/us-va.yml +567 -0
  158. data/data/plates/us-vi.yml +489 -0
  159. data/data/plates/us-vt.yml +458 -0
  160. data/data/plates/us-wa.yml +484 -0
  161. data/data/plates/us-wi.yml +509 -0
  162. data/data/plates/us-wv.yml +446 -0
  163. data/data/plates/us-wy.yml +523 -0
  164. data/data/plates/va.yml +604 -0
  165. data/data/plates/za.yml +1141 -0
  166. data/data/vehicles.json +160653 -105090
  167. data/lib/vehicles/configuration.rb +17 -3
  168. data/lib/vehicles/dataset.rb +73 -18
  169. data/lib/vehicles/fuzzy.rb +115 -0
  170. data/lib/vehicles/http.rb +48 -0
  171. data/lib/vehicles/make.rb +36 -6
  172. data/lib/vehicles/mcp_server.rb +5 -5
  173. data/lib/vehicles/model.rb +21 -2
  174. data/lib/vehicles/plates/series.rb +172 -18
  175. data/lib/vehicles/plates/template_fill.rb +188 -0
  176. data/lib/vehicles/plates/templates.rb +162 -0
  177. data/lib/vehicles/plates.rb +12 -0
  178. data/lib/vehicles/providers/hosted_provider.rb +28 -55
  179. data/lib/vehicles/validators/vehicle_make_validator.rb +3 -1
  180. data/lib/vehicles/validators/vehicle_model_validator.rb +1 -1
  181. data/lib/vehicles/version.rb +1 -1
  182. data/lib/vehicles.rb +32 -16
  183. data/rakelib/data_refresh.rake +109 -0
  184. data/rakelib/plates_refresh.rake +29 -0
  185. data/sig/vehicles.rbs +18 -0
  186. metadata +164 -2
@@ -4,34 +4,38 @@ require "json"
4
4
 
5
5
  module Vehicles
6
6
  module Providers
7
- # Talks to the hosted VehiclesDB API to enrich models with years, images, and
8
- # segments. It is STRICTLY OPTIONAL: `available?` is true only when an api_key
9
- # is configured, so a key-less install never reaches the network. Every call
10
- # is error-isolated — a slow, missing, or failing API yields nil and the
11
- # resolver falls back to the local data. Tracking/enrichment must never break
12
- # the host app, so this never raises out.
7
+ # Talks to the hosted VehiclesDB API for vehicle imagery. It is STRICTLY
8
+ # OPTIONAL: `available?` is true only when an api_key is configured, so a
9
+ # key-less install never reaches the network. Every call is error-isolated
10
+ # — a slow, missing, or failing API yields nil and the resolver falls back
11
+ # to the local data. Enrichment must never break the host app, so this
12
+ # never raises out.
13
13
  #
14
14
  # The images endpoint is LIVE (since 2026-07): mint a key at
15
15
  # https://vehiclesdb.com/settings/api-keys, set `config.api_key`, and
16
16
  # `model.image` / `model.images` answer with rendered vehicle imagery.
17
- # The enrichment endpoints (years/segment) are still the wired-up seam —
18
- # they safely return nil until the service ships them.
17
+ #
18
+ # `years` and `segment` are the wired-up seam for enrichment the API does
19
+ # not serve yet: they answer nil WITHOUT a request (a call to an endpoint
20
+ # that doesn't exist would only cost latency), so the chain falls through
21
+ # to the local data. They light up here when the API ships them.
19
22
  module HostedProvider
23
+ # Every VehiclesDB API endpoint lives under this path on
24
+ # `config.api_base_url` (https://vehiclesdb.com/api/v1/...).
25
+ API_PREFIX = "/api/v1"
26
+
20
27
  module_function
21
28
 
22
29
  def available?
23
30
  !Vehicles.configuration.api_key.to_s.empty?
24
31
  end
25
32
 
26
- def years(model)
27
- data = fetch(model)
28
- return nil unless data && data["year_start"]
29
-
30
- (data["year_start"]..data["year_end"]) # year_end may be nil => endless range
33
+ def years(_model)
34
+ nil
31
35
  end
32
36
 
33
- def segment(model)
34
- fetch(model)&.dig("segment")&.to_sym
37
+ def segment(_model)
38
+ nil
35
39
  end
36
40
 
37
41
  # One variant URL — the common "just give me an <img src>" case.
@@ -46,7 +50,7 @@ module Vehicles
46
50
  # the API actually served, `requested_color` what you asked for (a color
47
51
  # that isn't rendered yet falls back rather than 404ing).
48
52
  #
49
- # GET /v1/vehicles/:kind/:make_slug/:model_slug/images?color=<slug>
53
+ # GET /api/v1/vehicles/:kind/:make_slug/:model_slug/images?color=<slug>
50
54
  #
51
55
  # `year`/`trim` filters are RESERVED server-side today (the API 422s on
52
56
  # them by contract, so callers can't silently build on unimplemented
@@ -55,50 +59,19 @@ module Vehicles
55
59
  def images(model, color: nil)
56
60
  params = {}
57
61
  params[:color] = color.to_s unless color.to_s.empty?
58
- get("/v1/vehicles/#{model.kind}/#{model.make_slug}/#{model.model_slug}/images", params)
62
+ get("#{API_PREFIX}/vehicles/#{model.kind}/#{model.make_slug}/#{model.model_slug}/images", params)
59
63
  end
60
64
 
61
65
  # --- internals -----------------------------------------------------------
62
66
 
63
- # Fetch + memoize the full model payload for this process. Keyed by slug.
64
- def fetch(model)
65
- @cache ||= {}
66
- return @cache[model.slug] if @cache.key?(model.slug)
67
-
68
- @cache[model.slug] = get("/v1/models/#{model.slug}", {})
69
- end
70
-
71
- def reset!
72
- @cache = {}
73
- end
74
-
75
- # Issue a GET and parse JSON. Returns nil on ANY failure (network, timeout,
76
- # non-200, bad JSON) — callers treat nil as "fall back to local data".
67
+ # Issue a keyed GET and parse JSON. Returns nil on ANY failure (network,
68
+ # timeout, non-200, bad JSON) — callers treat nil as "fall back to local
69
+ # data". The wire rules (timeout, no retry) are Vehicles::HTTP's.
77
70
  def get(path, params)
78
- # Lazy-required: the gem is standalone-first, and this whole module is
79
- # inert without an api_key, so we don't pay net/http at load time.
80
- require "net/http"
81
- require "uri"
82
-
83
- config = Vehicles.configuration
84
- uri = URI.join(config.api_base_url, path)
85
- uri.query = URI.encode_www_form(params) unless params.empty?
86
-
87
- http = Net::HTTP.new(uri.host, uri.port)
88
- http.use_ssl = uri.scheme == "https"
89
- http.open_timeout = config.api_timeout
90
- http.read_timeout = config.api_timeout
91
-
92
- request = Net::HTTP::Get.new(uri)
93
- request["Authorization"] = "Bearer #{config.api_key}"
94
- request["Accept"] = "application/json"
95
-
96
- response = http.request(request)
97
- return nil unless response.is_a?(Net::HTTPSuccess)
98
-
99
- JSON.parse(response.body)
100
- rescue StandardError => e
101
- warn "[vehicles] hosted lookup failed (#{e.class}); using local data" if $DEBUG
71
+ body = HTTP.get(HTTP.url(path, params),
72
+ "Authorization" => "Bearer #{Vehicles.configuration.api_key}", "Accept" => "application/json")
73
+ body && JSON.parse(body)
74
+ rescue StandardError # bad JSON, a malformed api_base_url
102
75
  nil
103
76
  end
104
77
  end
@@ -15,7 +15,9 @@ class VehicleMakeValidator < ActiveModel::EachValidator
15
15
  def validate_each(record, attribute, value)
16
16
  return if value.blank?
17
17
  return if options[:allow_other] && Vehicles.other?(value)
18
- return if Vehicles.make(value)
18
+ # Exact-only: a typo must fail validation, not be stored as if it were the
19
+ # make it resembles (Vehicles.make is typo-tolerant since 0.7.8).
20
+ return if Vehicles.make(value, fuzzy: false)
19
21
 
20
22
  record.errors.add(attribute, options[:message] || "is not a recognized vehicle make")
21
23
  rescue StandardError => e
@@ -20,7 +20,7 @@ class VehicleModelValidator < ActiveModel::EachValidator
20
20
 
21
21
  make_attribute = options[:make] || :make
22
22
  make_value = record.respond_to?(make_attribute) ? record.public_send(make_attribute) : nil
23
- make = Vehicles.make(make_value)
23
+ make = Vehicles.make(make_value, fuzzy: false) # exact, same rule as the make validator
24
24
  return if make.nil? # unknown make -> defer to the make validator
25
25
  return if make.model(value)
26
26
 
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Vehicles
4
- VERSION = "0.6.1"
4
+ VERSION = "0.8.0"
5
5
  end
data/lib/vehicles.rb CHANGED
@@ -2,9 +2,11 @@
2
2
 
3
3
  require_relative "vehicles/version"
4
4
  require_relative "vehicles/configuration"
5
+ require_relative "vehicles/http"
5
6
  require_relative "vehicles/make"
6
7
  require_relative "vehicles/model"
7
8
  require_relative "vehicles/color"
9
+ require_relative "vehicles/fuzzy"
8
10
  require_relative "vehicles/dataset"
9
11
  require_relative "vehicles/refresher"
10
12
  require_relative "vehicles/providers/local_provider"
@@ -13,6 +15,8 @@ require_relative "vehicles/other_option"
13
15
  require_relative "vehicles/plates"
14
16
  require_relative "vehicles/plates/jurisdiction"
15
17
  require_relative "vehicles/plates/series"
18
+ require_relative "vehicles/plates/template_fill"
19
+ require_relative "vehicles/plates/templates"
16
20
 
17
21
  # Car makes & models for your Rails app — dropdowns, search, validation. Bundled
18
22
  # data, zero config, no network calls. Standalone first; an SDK for the hosted
@@ -46,7 +50,6 @@ module Vehicles
46
50
  @providers = nil
47
51
  @data_path = nil
48
52
  Dataset.reset!
49
- Providers::HostedProvider.reset!
50
53
  end
51
54
 
52
55
  # --- data access ---------------------------------------------------------
@@ -111,20 +114,28 @@ module Vehicles
111
114
  # Vehicles.models("Toyota", include_other: true) # ... + the "Other" escape hatch
112
115
  # With `include_other:`, an unknown make (e.g. the "Other" make itself) yields
113
116
  # just `[other_label]`, so a make→model picker never dead-ends.
114
- def models(make, kind: nil, body_type: nil, region: nil, rarity: nil, max_decile: nil, include_other: false)
115
- names = make(make)&.models(kind: kind, body_type: body_type, region: region || configuration.region,
116
- rarity: rarity, max_decile: max_decile)&.map(&:name) || []
117
+ # `fuzzy:` is forwarded to `make` (typo-tolerant by default since 0.7.8).
118
+ def models(make, kind: nil, body_type: nil, region: nil, rarity: nil, max_decile: nil, include_other: false,
119
+ fuzzy: true)
120
+ names = make(make, fuzzy: fuzzy)&.models(kind: kind, body_type: body_type,
121
+ region: region || configuration.region,
122
+ rarity: rarity, max_decile: max_decile)&.map(&:name) || []
117
123
  append_other_name(names, include_other)
118
124
  end
119
125
 
120
- # The rich Make object (or nil). Forgiving: name, slug, or alias.
121
- def make(query)
122
- dataset.find_make(query)
126
+ # The rich Make object (or nil). Forgiving: name, slug, alias — and, since
127
+ # 0.7.8, typos: "marcedes" → Mercedes-Benz, "volkswagon" → Volkswagen
128
+ # (bounded edit distance, only when ONE make is closest; ties → nil).
129
+ # Pass `fuzzy: false` for exact-only resolution (the validators do).
130
+ def make(query, fuzzy: true)
131
+ dataset.find_make(query, fuzzy: fuzzy)
123
132
  end
124
133
 
125
134
  # Resolve a free-text "make + model" string into one Model (or nil).
126
- def find(query)
127
- dataset.find_model(query)
135
+ # Exact makes are tried first; a misspelled make is retried with the same
136
+ # typo tolerance as `make` ("volkswagon golf"). `fuzzy: false` disables it.
137
+ def find(query, fuzzy: true)
138
+ dataset.find_model(query, fuzzy: fuzzy)
128
139
  end
129
140
 
130
141
  # Resolve a stored make + model PAIR into a Model (or nil). The structured
@@ -133,9 +144,13 @@ module Vehicles
133
144
  # model's metadata (kind, body_type, …) back.
134
145
  # Vehicles.model("Audi", "A3") # => #<Vehicles::Model "Audi A3">
135
146
  # Vehicles.model("vw", "golf") # forgiving, like every other lookup
136
- def model(make_name, model_name)
137
- found = make(make_name)
138
- found&.model(model_name)
147
+ # Vehicles.model("Kia", "XCeed") # spacing-blind: the dataset's "X Ceed"
148
+ # Vehicles.model("BMW", "Serie 1") # => BMW 1 Series
149
+ # Vehicles.model("Toyota", "Land Cruiser") # => the car (preferred)
150
+ # Vehicles.model("Toyota", "Land Cruiser", kind: :bus)
151
+ def model(make_name, model_name, fuzzy: true, kind: nil)
152
+ found = make(make_name, fuzzy: fuzzy)
153
+ found&.model(model_name, kind: kind)
139
154
  end
140
155
 
141
156
  # License plates (the bundled PRD-PLATES dataset). No argument: every
@@ -170,8 +185,9 @@ module Vehicles
170
185
  # first. Filter by country (ISO alpha-2) or continent (:eu/:as/…).
171
186
  # Vehicles.top_models(kind: :car, country: :nl, limit: 10).map(&:name)
172
187
  # Vehicles.top_models(kind: :motorcycle, region: :as, limit: 10)
173
- def top_models(kind: nil, country: nil, region: nil, limit: 20)
174
- dataset.top_models(kind: kind, country: country, region: region, limit: limit)
188
+ # Vehicles.top_models(kind: :car, by: :mass_decile) # by registration mass (data 2026.10.1+)
189
+ def top_models(kind: nil, country: nil, region: nil, limit: 20, by: :global_decile)
190
+ dataset.top_models(kind: kind, country: country, region: region, limit: limit, by: by)
175
191
  end
176
192
 
177
193
  # A curated slice of models by kind/continent/rarity — the "give me sensible
@@ -219,8 +235,8 @@ module Vehicles
219
235
 
220
236
  # [[label, value], ...] of a make's models for a Rails `select`. Unknown => [].
221
237
  # Vehicles.model_options("audi", include_other: true) # ... + [other_label, "other"]
222
- def model_options(make, kind: nil, body_type: nil, include_other: false)
223
- found = make(make)
238
+ def model_options(make, kind: nil, body_type: nil, include_other: false, fuzzy: true)
239
+ found = make(make, fuzzy: fuzzy)
224
240
  opts = found ? found.model_options(kind: kind, body_type: body_type) : []
225
241
  append_other_option(opts, include_other)
226
242
  end
@@ -0,0 +1,109 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "English"
4
+ require "json"
5
+
6
+ # The bundled dataset refresh (data/vehicles.json), as a task so the
7
+ # provenance step and the country-rank merge can't be forgotten.
8
+ #
9
+ # rake "data:refresh[v2026.10.0]" # a published release TAG, never a branch
10
+ #
11
+ # Source: the public data repo at a release tag — dist/vehicles.json (the gem
12
+ # projection) unchanged, EXCEPT that each model gains `country_ranks`
13
+ # ({ "gb" => 2, … }) when the projection does not carry it yet. Those ranks
14
+ # are copied from the SAME tag's catalog/<kind>/models.json
15
+ # (popularity.by_country[cc].rank — open CC-BY data), keyed by the full
16
+ # kind/make/model id, so the merged file is what the pipeline's own projection
17
+ # emits once it ships the key (vehiclesdb-pipeline emit.rb write_dist_json!,
18
+ # 2026-10-10). When the projection DOES carry the key, the task asserts every
19
+ # rank equals the catalog's and aborts on any mismatch. Output formatting is
20
+ # the pipeline's (JSON.pretty_generate + "\n"); stripping `country_ranks`
21
+ # from the output gives back the release file byte for byte.
22
+ module DataRefresh
23
+ module_function
24
+
25
+ def git_show(repo, rev, path)
26
+ out = IO.popen(["git", "-C", repo, "show", "#{rev}:#{path}"], &:read)
27
+ abort "missing #{path} at #{rev}" unless $CHILD_STATUS.success?
28
+ out
29
+ end
30
+
31
+ # "kind/make/model" => { "gb" => 2, … } for every ranked model at the tag.
32
+ def catalog_ranks(repo, rev, kinds)
33
+ kinds.each_with_object({}) do |kind, ranks|
34
+ JSON.parse(git_show(repo, rev, "catalog/#{kind}/models.json")).each do |m|
35
+ by_country = m.dig("popularity", "by_country") || {}
36
+ r = by_country.filter_map { |cc, p| [cc, p["rank"]] if p["rank"] }.sort.to_h
37
+ ranks["#{kind}/#{m["id"]}"] = r unless r.empty?
38
+ end
39
+ end
40
+ end
41
+
42
+ # Insert `country_ranks` where the pipeline emits it (right after
43
+ # global_decile), or assert it when the projection already carries it.
44
+ # Returns [merged, native] counts.
45
+ def merge!(dist, ranks)
46
+ merged = native = 0
47
+ dist["makes"].each do |mk|
48
+ mk["models"].each do |m|
49
+ id = "#{m["kind"]}/#{mk["slug"]}/#{m["slug"]}"
50
+ want = ranks[id]
51
+ if m.key?("country_ranks")
52
+ native += 1
53
+ abort "country_ranks mismatch for #{id}" unless m["country_ranks"] == want
54
+ elsif want
55
+ m.replace(with_ranks(m, want))
56
+ merged += 1
57
+ end
58
+ end
59
+ end
60
+ [merged, native]
61
+ end
62
+
63
+ def with_ranks(model, ranks)
64
+ out = model.each_with_object({}) do |(k, v), h|
65
+ h[k] = v
66
+ h["country_ranks"] = ranks if k == "global_decile"
67
+ end
68
+ out["country_ranks"] ||= ranks
69
+ out
70
+ end
71
+
72
+ def provenance(tag, commit, version, merged, native)
73
+ <<~MD
74
+ # Bundled vehicles dataset
75
+
76
+ `data/vehicles.json` is the public data repo's `dist/vehicles.json`
77
+ (the gem projection) at a release tag:
78
+
79
+ - **Source**: https://github.com/vehiclesdb/vehiclesdb `dist/vehicles.json`
80
+ - **Tag**: `#{tag}` (commit `#{commit}`), dataset version `#{version}`
81
+ - **country_ranks**: #{native} model(s) carried it natively; #{merged} merged from
82
+ the same tag's `catalog/<kind>/models.json` `popularity.by_country[cc].rank`
83
+ - **License**: CC-BY 4.0 (see the data repo's LICENSE + ATTRIBUTION.md)
84
+
85
+ Refresh: `rake "data:refresh[<tag>]"` (this file is generated by it).
86
+ MD
87
+ end
88
+ end
89
+
90
+ namespace :data do
91
+ desc "Refresh data/vehicles.json from a release tag of the local dataset repo"
92
+ task :refresh, [:tag] do |_t, args|
93
+ tag = args[:tag] or abort 'usage: rake "data:refresh[vYYYY.MM.P]" (a release tag)'
94
+ root = File.expand_path("..", __dir__)
95
+ repo = File.expand_path(ENV.fetch("VDB_DATA_REPO", "~/GitHub/vehiclesdb"))
96
+ sh "git", "-C", repo, "fetch", "origin", "--tags", "-q"
97
+ # refs/tags/ only: a branch name or a bare SHA must not pass for a release.
98
+ commit = IO.popen(["git", "-C", repo, "rev-parse", "--verify", "-q", "refs/tags/#{tag}^{commit}"], &:read).strip
99
+ abort "not a release tag: #{tag}" unless $CHILD_STATUS.success? && !commit.empty?
100
+
101
+ # Read at the VERIFIED commit, not the tag name (a same-named branch must never win).
102
+ dist = JSON.parse(DataRefresh.git_show(repo, commit, "dist/vehicles.json"))
103
+ merged, native = DataRefresh.merge!(dist, DataRefresh.catalog_ranks(repo, commit, dist["kinds"]))
104
+ File.write("#{root}/data/vehicles.json", "#{JSON.pretty_generate(dist)}\n")
105
+ File.write("#{root}/data/PROVENANCE-vehicles.md",
106
+ DataRefresh.provenance(tag, commit, dist["version"], merged, native))
107
+ puts "data refreshed to #{tag} (#{dist["version"]}): #{merged} merged, #{native} native country_ranks"
108
+ end
109
+ end
@@ -0,0 +1,29 @@
1
+ # The plates-data refresh, as a task so the PROVENANCE step can't be
2
+ # forgotten (it is gem-local; a bare rm+archive deletes it silently —
3
+ # that shipped once in 0.7.4 and broke consumers reading the pin).
4
+ namespace :plates do
5
+ desc "Refresh data/plates from the local dataset repo's origin/main"
6
+ task :refresh do
7
+ root = File.expand_path("..", __dir__)
8
+ ds = File.expand_path("~/GitHub/vehiclesdb")
9
+ sh "git -C #{ds} fetch origin -q"
10
+ rm_rf "#{root}/data/plates"
11
+ mkdir_p "#{root}/data/plates"
12
+ sh "git -C #{ds} archive origin/main plates | tar -x --exclude 'plates/_art' -C #{root}/data/"
13
+ rm_rf "#{root}/data/plates/_art"
14
+ commit = `git -C #{ds} rev-parse origin/main`.strip
15
+ File.write("#{root}/data/plates/PROVENANCE.md", <<~MD)
16
+ # Bundled plates dataset (PRD-PLATES, full corpus)
17
+
18
+ Copy of `plates/` from the public data repo (EXCLUDING `_art/`),
19
+ refreshed on gem releases:
20
+
21
+ - **Source**: https://github.com/vehiclesdb/vehiclesdb `plates/`
22
+ - **Commit**: `#{commit}`
23
+ - **License**: CC-BY 4.0 (see the data repo's LICENSE + ATTRIBUTION.md)
24
+
25
+ Refresh: `rake plates:refresh` (this file is generated by it).
26
+ MD
27
+ puts "plates refreshed at #{commit}"
28
+ end
29
+ end
data/sig/vehicles.rbs CHANGED
@@ -49,6 +49,24 @@ module Vehicles
49
49
  attr_accessor cache_path: String
50
50
  attr_accessor use_cache: bool
51
51
  attr_accessor refresh_timeout: Integer
52
+ attr_accessor plate_template_cache: untyped
53
+ attr_accessor plate_template_ttl: Integer
54
+ end
55
+
56
+ module Plates
57
+ def self.template: (code: String | Symbol, series: String, ?shape: String?, ?region: String?) -> String?
58
+ def self.render: (String registration, jurisdiction: String | Symbol, ?region: String?, ?label: String?) -> String?
59
+
60
+ module TemplateFill
61
+ class Error < StandardError
62
+ end
63
+
64
+ SCHEMA_VERSION: Integer
65
+ def self.fill: (String svg, String registration, ?label: String?) -> String
66
+ def self.shape_of: (String text) -> String?
67
+ def self.fit_size: (Numeric size, Numeric room, Numeric natural_width) -> Float
68
+ def self.recentered_baseline: (Numeric origin, Numeric size, Numeric per_size) -> Float
69
+ end
52
70
  end
53
71
 
54
72
  class Make
metadata CHANGED
@@ -1,13 +1,13 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: vehicles
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.6.1
4
+ version: 0.8.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - rameerez
8
8
  bindir: exe
9
9
  cert_chain: []
10
- date: 2026-08-01 00:00:00.000000000 Z
10
+ date: 2026-11-09 00:00:00.000000000 Z
11
11
  dependencies: []
12
12
  description: vehicles ships a curated, bundled dataset of car makes and models (with
13
13
  kind and body type) and a delightful, Rails-friendly API for vehicle dropdowns,
@@ -21,16 +21,172 @@ executables:
21
21
  extensions: []
22
22
  extra_rdoc_files: []
23
23
  files:
24
+ - ".rubocop_todo.yml"
24
25
  - CHANGELOG.md
25
26
  - LICENSE.txt
26
27
  - README.md
28
+ - data/PROVENANCE-vehicles.md
27
29
  - data/plates/PROVENANCE.md
30
+ - data/plates/_decode/at-districts.yml
31
+ - data/plates/_decode/ch-cantons.yml
32
+ - data/plates/_decode/cn-issuing-offices.yml
33
+ - data/plates/_decode/cn-provinces.yml
34
+ - data/plates/_decode/cz-kraj.yml
35
+ - data/plates/_decode/dk-serial-blocks.yml
28
36
  - data/plates/_decode/es-provinces.yml
37
+ - data/plates/_decode/fr-departements.yml
38
+ - data/plates/_decode/gb-age-identifiers.yml
39
+ - data/plates/_decode/gb-memory-tags.yml
40
+ - data/plates/_decode/gr-prefectures.yml
41
+ - data/plates/_decode/hr-cities.yml
42
+ - data/plates/_decode/ie-counties.yml
43
+ - data/plates/_decode/in-state-codes.yml
44
+ - data/plates/_decode/is-districts-1938.yml
45
+ - data/plates/_decode/it-provinces.yml
46
+ - data/plates/_decode/jp-office-marks.yml
47
+ - data/plates/_decode/jp-use-marks.yml
48
+ - data/plates/_decode/kr-authority-marks.yml
49
+ - data/plates/_decode/kr-use-syllables.yml
50
+ - data/plates/_decode/lu-series-order.yml
51
+ - data/plates/_decode/mx-entity-series.yml
52
+ - data/plates/_decode/my-index-marks.yml
53
+ - data/plates/_decode/no-counties-1913.yml
54
+ - data/plates/_decode/no-districts.yml
55
+ - data/plates/_decode/pl-powiats.yml
56
+ - data/plates/_decode/pt-trailer-services.yml
57
+ - data/plates/_decode/ro-counties.yml
58
+ - data/plates/_decode/se-diplomatic.yml
59
+ - data/plates/_decode/si-regions.yml
60
+ - data/plates/_decode/sk-districts.yml
61
+ - data/plates/_decode/th-registration-elements.yml
62
+ - data/plates/_decode/ua-regions.yml
63
+ - data/plates/_decode/za-licence-marks.yml
29
64
  - data/plates/_meta/classes.yml
65
+ - data/plates/_meta/separators.yml
66
+ - data/plates/ad.yml
67
+ - data/plates/ar.yml
68
+ - data/plates/at.yml
69
+ - data/plates/au-act.yml
70
+ - data/plates/au-nsw.yml
71
+ - data/plates/au-nt.yml
72
+ - data/plates/au-qld.yml
73
+ - data/plates/au-sa.yml
74
+ - data/plates/au-tas.yml
75
+ - data/plates/au-vic.yml
76
+ - data/plates/au-wa.yml
77
+ - data/plates/be.yml
78
+ - data/plates/br.yml
79
+ - data/plates/ca-ab.yml
80
+ - data/plates/ca-bc.yml
81
+ - data/plates/ca-mb.yml
82
+ - data/plates/ca-nb.yml
83
+ - data/plates/ca-nl.yml
84
+ - data/plates/ca-ns.yml
85
+ - data/plates/ca-nt.yml
86
+ - data/plates/ca-nu.yml
87
+ - data/plates/ca-on.yml
88
+ - data/plates/ca-pe.yml
89
+ - data/plates/ca-qc.yml
90
+ - data/plates/ca-sk.yml
91
+ - data/plates/ca-yt.yml
92
+ - data/plates/ch.yml
93
+ - data/plates/cn.yml
94
+ - data/plates/cy.yml
95
+ - data/plates/cz.yml
30
96
  - data/plates/de.yml
97
+ - data/plates/dk.yml
98
+ - data/plates/ee.yml
31
99
  - data/plates/es.yml
100
+ - data/plates/fi.yml
101
+ - data/plates/fr.yml
102
+ - data/plates/gb.yml
103
+ - data/plates/gr.yml
104
+ - data/plates/hr.yml
105
+ - data/plates/hu.yml
106
+ - data/plates/ie.yml
107
+ - data/plates/in.yml
108
+ - data/plates/is.yml
109
+ - data/plates/it.yml
110
+ - data/plates/jp.yml
111
+ - data/plates/kr.yml
112
+ - data/plates/li.yml
113
+ - data/plates/lt.yml
114
+ - data/plates/lu.yml
115
+ - data/plates/lv.yml
116
+ - data/plates/mc.yml
117
+ - data/plates/mt.yml
118
+ - data/plates/mx.yml
119
+ - data/plates/my.yml
32
120
  - data/plates/nl.yml
121
+ - data/plates/no.yml
122
+ - data/plates/nz.yml
123
+ - data/plates/pl.yml
124
+ - data/plates/pt.yml
125
+ - data/plates/ro.yml
126
+ - data/plates/se.yml
127
+ - data/plates/si.yml
128
+ - data/plates/sk.yml
129
+ - data/plates/sm.yml
130
+ - data/plates/th.yml
131
+ - data/plates/ua.yml
132
+ - data/plates/us-ak.yml
133
+ - data/plates/us-al.yml
134
+ - data/plates/us-ar.yml
135
+ - data/plates/us-as.yml
136
+ - data/plates/us-az.yml
137
+ - data/plates/us-ca.yml
138
+ - data/plates/us-co.yml
139
+ - data/plates/us-ct.yml
140
+ - data/plates/us-dc.yml
141
+ - data/plates/us-de.yml
33
142
  - data/plates/us-fl.yml
143
+ - data/plates/us-ga.yml
144
+ - data/plates/us-gu.yml
145
+ - data/plates/us-hi.yml
146
+ - data/plates/us-ia.yml
147
+ - data/plates/us-id.yml
148
+ - data/plates/us-il.yml
149
+ - data/plates/us-in.yml
150
+ - data/plates/us-ks.yml
151
+ - data/plates/us-ky.yml
152
+ - data/plates/us-la.yml
153
+ - data/plates/us-ma.yml
154
+ - data/plates/us-md.yml
155
+ - data/plates/us-me.yml
156
+ - data/plates/us-mi.yml
157
+ - data/plates/us-mn.yml
158
+ - data/plates/us-mo.yml
159
+ - data/plates/us-mp.yml
160
+ - data/plates/us-ms.yml
161
+ - data/plates/us-mt.yml
162
+ - data/plates/us-nc.yml
163
+ - data/plates/us-nd.yml
164
+ - data/plates/us-ne.yml
165
+ - data/plates/us-nh.yml
166
+ - data/plates/us-nj.yml
167
+ - data/plates/us-nm.yml
168
+ - data/plates/us-nv.yml
169
+ - data/plates/us-ny.yml
170
+ - data/plates/us-oh.yml
171
+ - data/plates/us-ok.yml
172
+ - data/plates/us-or.yml
173
+ - data/plates/us-pa.yml
174
+ - data/plates/us-pr.yml
175
+ - data/plates/us-ri.yml
176
+ - data/plates/us-sc.yml
177
+ - data/plates/us-sd.yml
178
+ - data/plates/us-tn.yml
179
+ - data/plates/us-tx.yml
180
+ - data/plates/us-ut.yml
181
+ - data/plates/us-va.yml
182
+ - data/plates/us-vi.yml
183
+ - data/plates/us-vt.yml
184
+ - data/plates/us-wa.yml
185
+ - data/plates/us-wi.yml
186
+ - data/plates/us-wv.yml
187
+ - data/plates/us-wy.yml
188
+ - data/plates/va.yml
189
+ - data/plates/za.yml
34
190
  - data/vehicles.json
35
191
  - exe/vehicles-mcp
36
192
  - lib/generators/vehicles/install_generator.rb
@@ -41,6 +197,8 @@ files:
41
197
  - lib/vehicles/colors.rb
42
198
  - lib/vehicles/configuration.rb
43
199
  - lib/vehicles/dataset.rb
200
+ - lib/vehicles/fuzzy.rb
201
+ - lib/vehicles/http.rb
44
202
  - lib/vehicles/make.rb
45
203
  - lib/vehicles/mcp_server.rb
46
204
  - lib/vehicles/model.rb
@@ -48,6 +206,8 @@ files:
48
206
  - lib/vehicles/plates.rb
49
207
  - lib/vehicles/plates/jurisdiction.rb
50
208
  - lib/vehicles/plates/series.rb
209
+ - lib/vehicles/plates/template_fill.rb
210
+ - lib/vehicles/plates/templates.rb
51
211
  - lib/vehicles/providers/hosted_provider.rb
52
212
  - lib/vehicles/providers/local_provider.rb
53
213
  - lib/vehicles/railtie.rb
@@ -55,6 +215,8 @@ files:
55
215
  - lib/vehicles/validators/vehicle_make_validator.rb
56
216
  - lib/vehicles/validators/vehicle_model_validator.rb
57
217
  - lib/vehicles/version.rb
218
+ - rakelib/data_refresh.rake
219
+ - rakelib/plates_refresh.rake
58
220
  - sig/vehicles.rbs
59
221
  homepage: https://github.com/vehiclesdb/vehicles
60
222
  licenses: