vehicles 0.9.0 → 0.9.2
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 +4 -4
- data/CHANGELOG.md +57 -7
- data/README.md +353 -69
- data/data/PROVENANCE-vehicles.md +2 -2
- data/data/plates/PROVENANCE.md +2 -2
- data/data/plates/_decode/at-districts.yml +2 -2
- data/data/plates/_decode/cn-issuing-offices.yml +2 -2
- data/data/plates/_decode/cn-provinces.yml +2 -2
- data/data/plates/_decode/cz-kraj.yml +1 -1
- data/data/plates/_decode/dk-serial-blocks.yml +1 -1
- data/data/plates/_decode/es-provinces.yml +1 -1
- data/data/plates/_decode/fr-departements.yml +2 -2
- data/data/plates/_decode/gb-age-identifiers.yml +1 -1
- data/data/plates/_decode/gb-memory-tags.yml +1 -1
- data/data/plates/_decode/gr-prefectures.yml +1 -1
- data/data/plates/_decode/hr-cities.yml +3 -3
- data/data/plates/_decode/ie-counties.yml +3 -3
- data/data/plates/_decode/in-state-codes.yml +2 -2
- data/data/plates/_decode/it-provinces.yml +3 -3
- data/data/plates/_decode/jp-office-marks.yml +2 -2
- data/data/plates/_decode/jp-use-marks.yml +2 -2
- data/data/plates/_decode/kr-authority-marks.yml +1 -1
- data/data/plates/_decode/mx-entity-series.yml +1 -1
- data/data/plates/_decode/my-index-marks.yml +4 -4
- data/data/plates/_decode/pt-trailer-services.yml +1 -1
- data/data/plates/_decode/ro-counties.yml +1 -1
- data/data/plates/_decode/se-diplomatic.yml +1 -1
- data/data/plates/_decode/si-regions.yml +1 -1
- data/data/plates/_decode/ua-regions.yml +1 -1
- data/data/plates/_meta/classes.yml +1 -1
- data/data/plates/_meta/separators.yml +1 -1
- data/data/plates/ad.yml +555 -52
- data/data/plates/ar.yml +8 -8
- data/data/plates/at.yml +91 -10
- data/data/plates/au-act.yml +13 -13
- data/data/plates/au-nsw.yml +9 -9
- data/data/plates/au-nt.yml +13 -14
- data/data/plates/au-qld.yml +7 -7
- data/data/plates/au-sa.yml +7 -7
- data/data/plates/au-tas.yml +10 -10
- data/data/plates/au-vic.yml +6 -6
- data/data/plates/au-wa.yml +8 -8
- data/data/plates/be.yml +11 -11
- data/data/plates/br.yml +14 -14
- data/data/plates/ca-ab.yml +10 -10
- data/data/plates/ca-bc.yml +6 -6
- data/data/plates/ca-mb.yml +4 -4
- data/data/plates/ca-nb.yml +9 -9
- data/data/plates/ca-nl.yml +11 -11
- data/data/plates/ca-ns.yml +12 -12
- data/data/plates/ca-nt.yml +7 -7
- data/data/plates/ca-nu.yml +7 -7
- data/data/plates/ca-on.yml +21 -21
- data/data/plates/ca-pe.yml +13 -13
- data/data/plates/ca-qc.yml +11 -11
- data/data/plates/ca-sk.yml +6 -6
- data/data/plates/ca-yt.yml +6 -6
- data/data/plates/ch.yml +14 -14
- data/data/plates/cn.yml +9 -9
- data/data/plates/cy.yml +6 -6
- data/data/plates/cz.yml +9 -9
- data/data/plates/de.yml +8 -8
- data/data/plates/dk.yml +12 -12
- data/data/plates/ee.yml +13 -13
- data/data/plates/eg.yml +708 -0
- data/data/plates/es.yml +8 -8
- data/data/plates/fi.yml +14 -14
- data/data/plates/fr.yml +12 -12
- data/data/plates/gb.yml +16 -16
- data/data/plates/gr.yml +14 -14
- data/data/plates/hr.yml +15 -15
- data/data/plates/hu.yml +16 -16
- data/data/plates/ie.yml +7 -7
- data/data/plates/in.yml +7 -7
- data/data/plates/is.yml +3 -3
- data/data/plates/it.yml +21 -21
- data/data/plates/jp.yml +121 -121
- data/data/plates/kr.yml +6 -6
- data/data/plates/li.yml +6 -6
- data/data/plates/lt.yml +10 -10
- data/data/plates/lu.yml +4 -4
- data/data/plates/lv.yml +9 -9
- data/data/plates/mc.yml +4 -4
- data/data/plates/mt.yml +5 -5
- data/data/plates/mx.yml +7 -7
- data/data/plates/my.yml +18 -18
- data/data/plates/ng.yml +346 -0
- data/data/plates/nl.yml +5 -5
- data/data/plates/no.yml +7 -7
- data/data/plates/nz.yml +18 -18
- data/data/plates/pl.yml +4 -4
- data/data/plates/pt.yml +13 -5
- data/data/plates/ro.yml +14 -14
- data/data/plates/se.yml +13 -13
- data/data/plates/si.yml +8 -8
- data/data/plates/sk.yml +15 -15
- data/data/plates/sm.yml +7 -7
- data/data/plates/th.yml +10 -10
- data/data/plates/tr.yml +432 -0
- data/data/plates/ua.yml +9 -9
- data/data/plates/us-ak.yml +11 -11
- data/data/plates/us-al.yml +12 -12
- data/data/plates/us-ar.yml +15 -15
- data/data/plates/us-as.yml +10 -10
- data/data/plates/us-az.yml +10 -10
- data/data/plates/us-ca.yml +13 -13
- data/data/plates/us-co.yml +14 -14
- data/data/plates/us-ct.yml +24 -24
- data/data/plates/us-dc.yml +7 -7
- data/data/plates/us-de.yml +10 -10
- data/data/plates/us-fl.yml +17 -17
- data/data/plates/us-ga.yml +10 -10
- data/data/plates/us-gu.yml +11 -11
- data/data/plates/us-hi.yml +11 -11
- data/data/plates/us-ia.yml +9 -9
- data/data/plates/us-id.yml +13 -13
- data/data/plates/us-il.yml +11 -11
- data/data/plates/us-in.yml +9 -9
- data/data/plates/us-ks.yml +8 -8
- data/data/plates/us-ky.yml +11 -11
- data/data/plates/us-la.yml +11 -11
- data/data/plates/us-ma.yml +10 -10
- data/data/plates/us-md.yml +7 -7
- data/data/plates/us-me.yml +16 -16
- data/data/plates/us-mi.yml +8 -8
- data/data/plates/us-mn.yml +13 -13
- data/data/plates/us-mo.yml +13 -13
- data/data/plates/us-mp.yml +12 -12
- data/data/plates/us-ms.yml +8 -8
- data/data/plates/us-mt.yml +12 -12
- data/data/plates/us-nc.yml +12 -12
- data/data/plates/us-nd.yml +10 -10
- data/data/plates/us-ne.yml +10 -10
- data/data/plates/us-nh.yml +8 -8
- data/data/plates/us-nj.yml +8 -8
- data/data/plates/us-nm.yml +14 -14
- data/data/plates/us-nv.yml +18 -18
- data/data/plates/us-ny.yml +17 -17
- data/data/plates/us-oh.yml +13 -13
- data/data/plates/us-ok.yml +12 -12
- data/data/plates/us-or.yml +15 -15
- data/data/plates/us-pa.yml +14 -14
- data/data/plates/us-pr.yml +15 -15
- data/data/plates/us-ri.yml +10 -10
- data/data/plates/us-sc.yml +7 -7
- data/data/plates/us-sd.yml +12 -12
- data/data/plates/us-tn.yml +8 -8
- data/data/plates/us-tx.yml +17 -17
- data/data/plates/us-ut.yml +12 -12
- data/data/plates/us-va.yml +14 -14
- data/data/plates/us-vi.yml +16 -16
- data/data/plates/us-vt.yml +9 -9
- data/data/plates/us-wa.yml +18 -18
- data/data/plates/us-wi.yml +8 -8
- data/data/plates/us-wv.yml +9 -9
- data/data/plates/us-wy.yml +10 -10
- data/data/plates/va.yml +7 -7
- data/data/plates/za.yml +11 -11
- data/data/vehicles.json +121081 -62015
- data/lib/generators/vehicles/templates/initializer.rb +21 -12
- data/lib/generators/vehicles/templates/vehicles_refresh_job.rb +8 -4
- data/lib/vehicles/color.rb +8 -1
- data/lib/vehicles/colors.rb +14 -10
- data/lib/vehicles/configuration.rb +82 -3
- data/lib/vehicles/data_source.rb +220 -0
- data/lib/vehicles/dataset.rb +38 -6
- data/lib/vehicles/dataset_resolution.rb +39 -3
- data/lib/vehicles/dataset_slice.rb +90 -0
- data/lib/vehicles/hosted_result.rb +94 -0
- data/lib/vehicles/http.rb +75 -7
- data/lib/vehicles/id_lookup.rb +37 -0
- data/lib/vehicles/kind_rule.rb +63 -0
- data/lib/vehicles/labels.rb +131 -0
- data/lib/vehicles/make.rb +45 -19
- data/lib/vehicles/model.rb +66 -19
- data/lib/vehicles/model_match.rb +16 -2
- data/lib/vehicles/model_matcher.rb +6 -7
- data/lib/vehicles/plates/series.rb +87 -8
- data/lib/vehicles/plates/template_fill.rb +2 -2
- data/lib/vehicles/plates/templates.rb +9 -5
- data/lib/vehicles/plates.rb +54 -8
- data/lib/vehicles/providers/hosted_provider.rb +145 -32
- data/lib/vehicles/providers/local_provider.rb +2 -2
- data/lib/vehicles/refresh_result.rb +74 -0
- data/lib/vehicles/refresher.rb +190 -54
- data/lib/vehicles/release_checks.rb +72 -0
- data/lib/vehicles/text_matcher.rb +19 -10
- data/lib/vehicles/validators/vehicle_make_validator.rb +18 -1
- data/lib/vehicles/validators/vehicle_model_validator.rb +17 -1
- data/lib/vehicles/version.rb +1 -1
- data/lib/vehicles.rb +119 -67
- data/rakelib/data_refresh.rake +1 -1
- data/rakelib/plates_refresh.rake +1 -1
- data/sig/vehicles.rbs +67 -13
- metadata +13 -2
data/README.md
CHANGED
|
@@ -2,10 +2,12 @@
|
|
|
2
2
|
|
|
3
3
|
[](https://badge.fury.io/rb/vehicles) [](https://github.com/vehiclesdb/vehicles/actions)
|
|
4
4
|
|
|
5
|
+
> **Get your free key, the integration guide and the credit snippet at [vehiclesdb.com](https://vehiclesdb.com/users/sign_up?for=app_saas&cta=readme_vehicles)**
|
|
6
|
+
|
|
5
7
|
> [!TIP]
|
|
6
8
|
> **🚀 Ship your next Rails app 10x faster!** I've built **[RailsFast](https://railsfast.com/?ref=vehicles)**, a production-ready Rails boilerplate template that comes with everything you need to launch a software business in days, not weeks. Go [check it out](https://railsfast.com/?ref=vehicles)!
|
|
7
9
|
|
|
8
|
-
`vehicles` gives your Rails app a clean, curated list of vehicle makes and models —
|
|
10
|
+
`vehicles` gives your Rails app a clean, curated list of vehicle makes and models — **every make and model in the VehiclesDB open dataset: cars, motorcycles, mopeds, vans, trucks and buses**, reconciled from the official vehicle registers of every country the dataset covers (current counts: [vehiclesdb.com/makes](https://vehiclesdb.com/makes), or `manifest.json` in the data repo) — ready for dropdowns, search, and validation. No API keys, no database table, no migration — it works **fully offline** the second you `bundle install`, because the data ships inside the gem. (Optionally, it can [refresh](#staying-current-optional) the data without a gem upgrade.)
|
|
9
11
|
|
|
10
12
|
✨ Perfect for marketplaces, carpooling & rideshare apps, fleet tools, parking & EV-charging apps, insurance and booking forms — anywhere a user has to pick their vehicle.
|
|
11
13
|
|
|
@@ -62,7 +64,7 @@ Two methods cover 90% of what you need:
|
|
|
62
64
|
|
|
63
65
|
```ruby
|
|
64
66
|
Vehicles.makes
|
|
65
|
-
# => ["Abarth", "Acura", "Alfa Romeo", ...] (alphabetical,
|
|
67
|
+
# => ["Abarth", "Acura", "Alfa Romeo", ...] (alphabetical, every make)
|
|
66
68
|
|
|
67
69
|
Vehicles.models("Toyota")
|
|
68
70
|
# => ["Yaris", "Corolla", "Aygo", "RAV4", "C-HR", "Prius", ...]
|
|
@@ -155,7 +157,24 @@ It tries, strongest first, and stops at the first rung that answers:
|
|
|
155
157
|
- **No guessing from prefixes.** "C3 Aircr" is not "C3" (it's the C3 Aircross); only a curated form can say so.
|
|
156
158
|
- **Ids stay exact.** Slug-, id- and path-shaped input (`"golf-gti"`, `"car/volkswagen/golf"`, symbols) only takes the exact rungs.
|
|
157
159
|
|
|
158
|
-
`make.model(...)` and the `vehicle_model` validator stay strict — a form or a typo is not a valid model name to store.
|
|
160
|
+
`make.model(...)` and the `vehicle_model` validator stay strict — a form or a typo is not a valid model name to store.
|
|
161
|
+
|
|
162
|
+
**Safe to write back without asking?** `confident?` is the recommended auto-apply threshold: an answer (not ambiguous) from a curated rung — the id, a former id, the name, a published alias, a curated form, or one of those after `:cleaned`. Never `:fuzzy`: a typo correction is a guess to *show* the user ("did you mean Touareg?"), not to store silently.
|
|
163
|
+
|
|
164
|
+
```ruby
|
|
165
|
+
m = Vehicles.match_model(row.make, row.model, kind: :car)
|
|
166
|
+
row.update!(make: m.model.make, model: m.model.name, vehiclesdb_id: m.model.id) if m&.confident?
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
**Kinds.** One nameplate can be several records (Iveco's Daily is a van, a truck and a bus). That is not ambiguity: without a kind, every VehiclesDB surface (gem, API, SDKs, widget) applies the same rule — the record with the lowest `mass_decile`, then van → car → bus → truck → moped → motorcycle, then the id — so "Iveco Daily" is the van everywhere (`Vehicles::KindRule`; until 0.9.1 the gem preferred the car). Pass `kind:` to choose one, or `kinds:` to allow only some: a record outside `kinds:` never answers, on any rung, ids included.
|
|
170
|
+
|
|
171
|
+
```ruby
|
|
172
|
+
Vehicles.model("Iveco", "Daily") # => the van
|
|
173
|
+
Vehicles.model("Iveco", "Daily", kind: :bus) # => the bus
|
|
174
|
+
Vehicles.match_model("Iveco", "Daily", kinds: [:car]) # => nil (there is no car Daily)
|
|
175
|
+
Vehicles.identify("bus/iveco/daily", kinds: [:car]) # => nil (an id outside the scope too)
|
|
176
|
+
Vehicles.identify("bus/iveco/daily", kind: :car).model # => the bus: `kind:` never overrides an explicit id
|
|
177
|
+
```
|
|
159
178
|
|
|
160
179
|
One free-text string instead of a pair? `identify` splits off the make (the longest leading run that is a make) and runs the same ladder. It also takes canonical ids (`:exact`, 1.0), ids a record has replaced (`:former_id`, 0.98), and a whole string that is exactly one model's published alias (`:alias`, 0.90). Apart from those, nothing resolves without a make:
|
|
161
180
|
|
|
@@ -180,13 +199,14 @@ Every `Vehicles::Model` reads like English and serializes cleanly:
|
|
|
180
199
|
|
|
181
200
|
```ruby
|
|
182
201
|
car = Vehicles.find("seat leon")
|
|
202
|
+
car.id # => "car/seat/leon" ← the canonical id: store this
|
|
183
203
|
car.make # => "SEAT"
|
|
184
|
-
car.name # => "
|
|
185
|
-
car.full_name # => "SEAT
|
|
204
|
+
car.name # => "León"
|
|
205
|
+
car.full_name # => "SEAT León"
|
|
186
206
|
car.slug # => "seat-leon"
|
|
187
|
-
car.to_h # => { make: "SEAT", model: "
|
|
207
|
+
car.to_h # => { id: "car/seat/leon", make: "SEAT", model: "León", slug: "seat-leon",
|
|
188
208
|
# kind: :car, body_type: :hatchback, global_decile: 2,
|
|
189
|
-
# availability: ["es", "fi", "gb", ...] }
|
|
209
|
+
# availability: ["es", "fi", "gb", ...], ... }
|
|
190
210
|
```
|
|
191
211
|
|
|
192
212
|
## Kinds & body types
|
|
@@ -222,6 +242,15 @@ Vehicles.makes(kind: :motorcycle) # => makes that build bikes
|
|
|
222
242
|
Vehicles.search("golf").select(&:hatchback?)
|
|
223
243
|
```
|
|
224
244
|
|
|
245
|
+
Labels for a kind or body type `<select>` ship in English and Spanish (you store the symbol):
|
|
246
|
+
|
|
247
|
+
```ruby
|
|
248
|
+
Vehicles.kind_options(locale: :es) # => [["Autobús", "bus"], ["Camión", "truck"], ["Ciclomotor", "moped"], ["Coche", "car"], ...]
|
|
249
|
+
Vehicles.kind_label(:van, locale: :es) # => "Furgoneta"
|
|
250
|
+
Vehicles.body_type_label(:wagon, locale: :es) # => "Familiar"
|
|
251
|
+
Vehicles.body_type_options(locale: :es) # => [["Compacto", "hatchback"], ["Berlina", "sedan"], ...]
|
|
252
|
+
```
|
|
253
|
+
|
|
225
254
|
> [!NOTE]
|
|
226
255
|
> `body_type` is honest: cars always carry one; other kinds return `nil` until
|
|
227
256
|
> an honest vocabulary exists for them (a truck is not a "hatchback"). The
|
|
@@ -262,6 +291,8 @@ imports count, because they're real vehicles on real roads.
|
|
|
262
291
|
|
|
263
292
|
## MCP server for AI agents
|
|
264
293
|
|
|
294
|
+
> **Get your free key, the integration guide and the credit snippet at [vehiclesdb.com](https://vehiclesdb.com/users/sign_up?for=ai_app&cta=readme_vehicles_mcp)**
|
|
295
|
+
|
|
265
296
|
The gem ships `vehicles-mcp` — a read-only [MCP](https://modelcontextprotocol.io)
|
|
266
297
|
server over the bundled dataset, so your agents ground themselves in real
|
|
267
298
|
vehicle data instead of hallucinating nameplates. No network, no keys:
|
|
@@ -290,11 +321,16 @@ Vehicles.color("navy") # forgiving: synonyms, case, diacritics
|
|
|
290
321
|
Vehicles.color("navy").slug # => "blue" ← the stable value you store
|
|
291
322
|
Vehicles.color("navy").hex # => "#27408B" (representative swatch)
|
|
292
323
|
|
|
293
|
-
Vehicles.color_options
|
|
324
|
+
Vehicles.color_options # => [["White", "white"], ["Black", "black"], ...]
|
|
325
|
+
Vehicles.color_options(locale: :es) # => [["Blanco", "white"], ["Negro", "black"], ["Gris", "grey"], ...]
|
|
326
|
+
Vehicles.color("red").label(:es) # => "Rojo"
|
|
327
|
+
Vehicles.color("Marrón").slug # => "brown" — Spanish input resolves too ("rojo", "plateada", "granate")
|
|
328
|
+
Vehicles.colors(locale: :es).first # => #<Vehicles::Color white "Blanco" #F4F4F4>
|
|
294
329
|
```
|
|
295
330
|
|
|
296
|
-
|
|
297
|
-
|
|
331
|
+
Store the **slug** (it is also what maps to the image color variants); labels
|
|
332
|
+
ship in English and Spanish (`locale: "es-MX"` folds to `:es`; other locales
|
|
333
|
+
fall back to English).
|
|
298
334
|
|
|
299
335
|
## Rails dropdowns
|
|
300
336
|
|
|
@@ -310,17 +346,25 @@ Vehicles.model_options("audi")
|
|
|
310
346
|
|
|
311
347
|
### Dependent make → model picker
|
|
312
348
|
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
349
|
+
Scope the picker to what your app serves and embed
|
|
350
|
+
[`Vehicles.catalog`](#the-full-ruby-api) once: the model list switches with no
|
|
351
|
+
request at all — **no route, no controller, no fetch.** For a one-country app,
|
|
352
|
+
`country:` keeps it to the models with official evidence there, in that
|
|
353
|
+
country's own popularity order (Spain's cars: ~140 makes, ~1,300 models, ~14 KB
|
|
354
|
+
of JSON; the whole worldwide car catalog is ~63 KB):
|
|
355
|
+
|
|
356
|
+
```ruby
|
|
357
|
+
# app/helpers/vehicles_helper.rb
|
|
358
|
+
PICKER = { kind: :car, country: :es }.freeze
|
|
359
|
+
def vehicle_catalog = Vehicles.catalog(**PICKER)
|
|
360
|
+
```
|
|
317
361
|
|
|
318
362
|
```erb
|
|
319
363
|
<div data-controller="vehicle-picker"
|
|
320
|
-
data-vehicle-picker-catalog-value="<%=
|
|
321
|
-
<%= form.select :make,
|
|
364
|
+
data-vehicle-picker-catalog-value="<%= vehicle_catalog.to_json %>">
|
|
365
|
+
<%= form.select :make, vehicle_catalog.keys, { include_blank: "Make" },
|
|
322
366
|
data: { vehicle_picker_target: "make", action: "change->vehicle-picker#makeChanged" } %>
|
|
323
|
-
<%= form.select :model,
|
|
367
|
+
<%= form.select :model, vehicle_catalog.fetch(@car&.make.to_s, []), { include_blank: "Model" },
|
|
324
368
|
data: { vehicle_picker_target: "model" } %>
|
|
325
369
|
</div>
|
|
326
370
|
```
|
|
@@ -346,7 +390,74 @@ export default class extends Controller {
|
|
|
346
390
|
|
|
347
391
|
That's the whole picker — the gem ships the data, you keep your markup and ~12
|
|
348
392
|
lines of JS. (Storing slugs instead of names? Swap the selects for
|
|
349
|
-
`make_options` / `model_options` and key the catalog by slug.)
|
|
393
|
+
`make_options` / `model_options` and key the catalog by slug.) The same scope
|
|
394
|
+
also works on every list helper: `Vehicles.makes(kind: :car, country: :es)`,
|
|
395
|
+
`Vehicles.models("SEAT", kind: :car, country: :es)`, `make_options` /
|
|
396
|
+
`model_options(country:)`. Country evidence means *presence* in official data,
|
|
397
|
+
not an exhaustive list of what is legal there — keep an "Other" option.
|
|
398
|
+
|
|
399
|
+
### The make-model-dropdown widget, fed by your server
|
|
400
|
+
|
|
401
|
+
The [make-model-dropdown](https://github.com/vehiclesdb/make-model-dropdown)
|
|
402
|
+
widget (0.4.1+) does the picker for you: search, kept stored values, the
|
|
403
|
+
canonical id. `Vehicles.dataset_slice` takes the same scope as `catalog` and
|
|
404
|
+
returns the records behind the names, in the shape of the published
|
|
405
|
+
`dist/vehicles.json`, so the widget resolves stored values (aliases, curated
|
|
406
|
+
forms, former ids) and writes ids exactly as it would with the whole release.
|
|
407
|
+
Embed it and the page makes **no request** for vehicle data — no CDN, no API
|
|
408
|
+
key — and shows the release your server runs:
|
|
409
|
+
|
|
410
|
+
```ruby
|
|
411
|
+
# app/helpers/vehicles_helper.rb
|
|
412
|
+
# Built once per data release (Spain's cars: ~360 KB of JSON, ~57 KB gzipped).
|
|
413
|
+
def vehicles_dataset_json(**scope)
|
|
414
|
+
Rails.cache.fetch(["vehicles/dataset_slice", Vehicles.data_version, scope]) do
|
|
415
|
+
Vehicles.dataset_slice(**scope).to_json
|
|
416
|
+
end
|
|
417
|
+
end
|
|
418
|
+
```
|
|
419
|
+
|
|
420
|
+
```erb
|
|
421
|
+
<script type="application/json" id="vehicles-data"><%= json_escape(vehicles_dataset_json(kind: :car, country: :es)).html_safe %></script>
|
|
422
|
+
|
|
423
|
+
<div data-controller="vehicle-picker">
|
|
424
|
+
<%= form.select :make, [@car.make].compact, {}, data: { vehicle_picker_target: "make" } %>
|
|
425
|
+
<%= form.select :model, [@car.model].compact, {}, data: { vehicle_picker_target: "model" } %>
|
|
426
|
+
<%= form.hidden_field :vehiclesdb_id, data: { vehicle_picker_target: "id" } %>
|
|
427
|
+
</div>
|
|
428
|
+
```
|
|
429
|
+
|
|
430
|
+
```js
|
|
431
|
+
// app/javascript/controllers/vehicle_picker_controller.js
|
|
432
|
+
import { Controller } from "@hotwired/stimulus"
|
|
433
|
+
import VehiclesDropdown from "make-model-dropdown"
|
|
434
|
+
|
|
435
|
+
export default class extends Controller {
|
|
436
|
+
static targets = ["make", "model", "id"]
|
|
437
|
+
|
|
438
|
+
connect() {
|
|
439
|
+
const data = JSON.parse(document.getElementById("vehicles-data").textContent)
|
|
440
|
+
this.dropdown = VehiclesDropdown.attach({
|
|
441
|
+
data, kind: "car", country: "es", locale: "es", values: "name",
|
|
442
|
+
make: this.makeTarget, model: this.modelTarget, idInput: this.idTarget
|
|
443
|
+
})
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
disconnect() { this.dropdown.destroy() }
|
|
447
|
+
}
|
|
448
|
+
```
|
|
449
|
+
|
|
450
|
+
Give the widget the same `kind`/`country` you sliced by. What the slice
|
|
451
|
+
keeps: each make's `name`, `slug`, `kinds`, `aliases`, `former_slugs`; each
|
|
452
|
+
model's `name`, `slug`, `kind`, `aliases`, `forms`, `former_ids`,
|
|
453
|
+
`global_decile`, `mass_decile`, `availability`, `country_ranks` and
|
|
454
|
+
`registration_years`, plus the release's `version`, `schema_version`,
|
|
455
|
+
`license` and `attribution`. It leaves out `regions` and `body_type`, and with
|
|
456
|
+
`country:` it keeps only that country in `availability` and `country_ranks`
|
|
457
|
+
(every record in the slice is evidenced there; the other countries are most of
|
|
458
|
+
the bytes). The ids are not stored: they are `kind/make-slug/model-slug`, as
|
|
459
|
+
everywhere. The JS and Python SDKs read a slice too (dataset mode, when it is
|
|
460
|
+
what they load as `dist/vehicles.json`).
|
|
350
461
|
|
|
351
462
|
> [!TIP]
|
|
352
463
|
> Prefer a server round-trip (huge/remote dataset, or you'd rather not embed it)?
|
|
@@ -373,9 +484,16 @@ Car.new(make: "Tesler", model: "Model 3").valid? # => false
|
|
|
373
484
|
|
|
374
485
|
They're forgiving the same way the lookups are (aliases, case, slugs), and they never raise on blank/garbage input — they just add an error.
|
|
375
486
|
|
|
487
|
+
Scope them like your picker, so a value the picker never offered (a truck, a model never seen in your market) fails too:
|
|
488
|
+
|
|
489
|
+
```ruby
|
|
490
|
+
validates :make, vehicle_make: { kind: :car, country: :es }
|
|
491
|
+
validates :model, vehicle_model: { make: :make, kind: :car, country: :es } # kinds: [:car, :van] also works
|
|
492
|
+
```
|
|
493
|
+
|
|
376
494
|
### An "Other / not in the list" escape hatch
|
|
377
495
|
|
|
378
|
-
Make/model dropdowns should never be a dead end —
|
|
496
|
+
Make/model dropdowns should never be a dead end — the full model list is a lot, but
|
|
379
497
|
someone always shows up with the one car that isn't there. Turn on a first-class
|
|
380
498
|
"Other" option and the pieces agree by construction: `include_other:` adds it to
|
|
381
499
|
the picker, `allow_other:` lets it through validation.
|
|
@@ -424,17 +542,52 @@ specs/images) are *properties of that make+model* owned by the dataset — look
|
|
|
424
542
|
them up via the gem, don't duplicate them per row (they'd just drift). So the
|
|
425
543
|
schema is small and stable:
|
|
426
544
|
|
|
427
|
-
| Column
|
|
428
|
-
|
|
429
|
-
| `make`
|
|
430
|
-
| `model`
|
|
431
|
-
| `
|
|
432
|
-
| `
|
|
545
|
+
| Column | Type | Store | Example |
|
|
546
|
+
|-----------------|-----------|-----------------------------------------|-------------------------|
|
|
547
|
+
| `make` | `string` | display name | `"Volkswagen"` |
|
|
548
|
+
| `model` | `string` | display name | `"Golf"` |
|
|
549
|
+
| `vehiclesdb_id` | `string` | the canonical **id** (`Model#id`), nullable | `"car/volkswagen/golf"` |
|
|
550
|
+
| `year` | `integer` | the year | `2022` |
|
|
551
|
+
| `color` | `string` | a canonical color **slug** | `"blue"` |
|
|
433
552
|
|
|
434
|
-
Everything else (`kind`, `body_type`,
|
|
435
|
-
`Vehicles.
|
|
553
|
+
Everything else (`kind`, `body_type`, images, …) is derived from the id:
|
|
554
|
+
`Vehicles.by_id(record.vehiclesdb_id)`. No `kind`/`body_type` columns, no
|
|
436
555
|
migration to add new metadata later — it lands in the dataset, not your schema.
|
|
437
556
|
|
|
557
|
+
### Store the id
|
|
558
|
+
|
|
559
|
+
Names are for people; the **id** (`"car/volkswagen/golf"` — kind/make/model) is
|
|
560
|
+
what every VehiclesDB surface agrees on: the API, the JS/Python SDKs and the
|
|
561
|
+
widget take the same string, and it survives dataset releases. When a release
|
|
562
|
+
renames or merges a record, the new record lists the old id in `former_ids`, so a
|
|
563
|
+
stored id never dead-ends:
|
|
564
|
+
|
|
565
|
+
```ruby
|
|
566
|
+
car = Vehicles.model(params[:make], params[:model], kind: :car)
|
|
567
|
+
record.vehiclesdb_id = car&.id # nil for "Other" / unknown pairs
|
|
568
|
+
|
|
569
|
+
Vehicles.by_id("car/volkswagen/golf") # => #<Vehicles::Model "Volkswagen Golf">
|
|
570
|
+
Vehicles.by_id(old_id) # => the record that replaced it (follow: true)
|
|
571
|
+
Vehicles.by_id(old_id, follow: false) # => nil
|
|
572
|
+
Vehicles.live?(old_id) # => false
|
|
573
|
+
Vehicles.successor(old_id) # => the id to store now ("car/…/…")
|
|
574
|
+
```
|
|
575
|
+
|
|
576
|
+
After each data refresh, migrate stored ids in one pass:
|
|
577
|
+
|
|
578
|
+
```ruby
|
|
579
|
+
Car.where.not(vehiclesdb_id: nil).find_each do |car|
|
|
580
|
+
now = Vehicles.successor(car.vehiclesdb_id)
|
|
581
|
+
next if now.nil? || now == car.vehiclesdb_id
|
|
582
|
+
model = Vehicles.by_id(now)
|
|
583
|
+
car.update!(vehiclesdb_id: now, make: model.make, model: model.name)
|
|
584
|
+
end
|
|
585
|
+
```
|
|
586
|
+
|
|
587
|
+
Two records are equal when their ids are (`==`, `eql?`, `hash`): the van and the
|
|
588
|
+
bus of one nameplate are different records. A make that is renamed or merged
|
|
589
|
+
is followed too once the dataset publishes its `former_slugs`.
|
|
590
|
+
|
|
438
591
|
**Store the display name.** The simplest, most readable thing to persist is the
|
|
439
592
|
name itself (`"Volkswagen"`, `"Golf"`). Populate the selects with the plain
|
|
440
593
|
**name lists**, where the option value *is* the name:
|
|
@@ -471,10 +624,17 @@ car&.suv? # => false
|
|
|
471
624
|
```
|
|
472
625
|
|
|
473
626
|
It reads what people actually type: case, accents and spacing don't matter
|
|
474
|
-
(`"
|
|
627
|
+
(`"X Ceed"` is Kia's `"XCeed"`, `"A 5"` is `"A5"`), BMW's Spanish/Italian
|
|
475
628
|
`"Serie 3"` is the `"3 Series"`, and a name that exists as several kinds
|
|
476
|
-
(
|
|
477
|
-
pass `kind:` —
|
|
629
|
+
(Iveco's Daily: van, truck, bus) follows the [one kind rule](#matching-stored-make--model-pairs)
|
|
630
|
+
unless you pass `kind:` — a car-only app passes `kind: :car` everywhere.
|
|
631
|
+
|
|
632
|
+
**Year pickers.** `Model#registration_years` is a *hint* — `{ first:, last: }`, the first and last
|
|
633
|
+
year the nameplate shows up in official registrations, when the dataset
|
|
634
|
+
publishes it (nil until then) — good for ordering or pre-filling a year
|
|
635
|
+
`<select>`, never a validation bound (imports and late registrations fall
|
|
636
|
+
outside it legitimately). It is not production years: `Model#years` stays nil
|
|
637
|
+
until VehiclesDB publishes those.
|
|
478
638
|
|
|
479
639
|
**Only accept the kinds you support.** Filter every list/lookup by `kind:` so
|
|
480
640
|
the picker only ever offers vehicles your app handles — and widening later (when
|
|
@@ -486,9 +646,9 @@ Vehicles.makes(kind: :car) # makes that build cars
|
|
|
486
646
|
Vehicles.models("Toyota", kind: :car) # ...their cars only
|
|
487
647
|
```
|
|
488
648
|
|
|
489
|
-
No
|
|
490
|
-
|
|
491
|
-
|
|
649
|
+
No seed task, no API key — the data is bundled and read from memory. The
|
|
650
|
+
whole integration is the markup above plus the embedded catalog (see
|
|
651
|
+
[Rails dropdowns](#rails-dropdowns)).
|
|
492
652
|
|
|
493
653
|
## Country & region awareness
|
|
494
654
|
|
|
@@ -500,8 +660,8 @@ Vehicles.find("vw golf").available_in?(:nz) # => true
|
|
|
500
660
|
Vehicles.top_models(kind: :car, country: :es) # country-scoped popularity
|
|
501
661
|
```
|
|
502
662
|
|
|
503
|
-
The bundled snapshot is **global** (official registers of
|
|
504
|
-
Europe, North America, South America,
|
|
663
|
+
The bundled snapshot is **global** (official registers of every covered country, across
|
|
664
|
+
Europe, North America, South America, Asia, Oceania and Ukraine — see [Where the data
|
|
505
665
|
comes from](#where-the-data-comes-from)), so the region gate accepts every
|
|
506
666
|
region query — `region: :eu` callers from 0.1.x keep working unchanged:
|
|
507
667
|
|
|
@@ -512,7 +672,7 @@ Vehicles.region # => :global
|
|
|
512
672
|
|
|
513
673
|
## License plates
|
|
514
674
|
|
|
515
|
-
The gem bundles the VehiclesDB **registration-mark dataset
|
|
675
|
+
The gem bundles the VehiclesDB **registration-mark dataset**: every plate series per jurisdiction, researched to the issuing authority's own documents — formats, eras, alphabets, designs. Ask it whether a typed registration is real:
|
|
516
676
|
|
|
517
677
|
```ruby
|
|
518
678
|
Vehicles.plate("12-GB-BD", jurisdiction: :nl).valid? # => true — exact, as issued
|
|
@@ -520,11 +680,42 @@ Vehicles.plate("12gbbd", jurisdiction: :nl).suggestion # => "12-GB-BD" (did yo
|
|
|
520
680
|
Vehicles.plate("12-AB-CD", jurisdiction: :nl).strict? # => false — vowels aren't issued
|
|
521
681
|
```
|
|
522
682
|
|
|
523
|
-
Matching is **two-tier**: exact as-issued first, then a separator-forgiving tier — `"1234XYZ"`, `"1234 xyz"` and `"1234-XYZ"` all resolve to the same serial, with `suggestion` giving the formatting the plate actually wears. Leniency forgives punctuation, **never the alphabet**: the strict regexes encode what the authority really issues (the Dutch vowel purge, the Spanish consonant-triplet rule…). `Match#strict?` separates
|
|
683
|
+
Matching is **two-tier**: exact as-issued first, then a separator-forgiving tier — `"1234XYZ"`, `"1234 xyz"` and `"1234-XYZ"` all resolve to the same serial, with `suggestion` giving the formatting the plate actually wears. Leniency forgives punctuation, **never the alphabet**: the strict regexes encode what the authority really issues (the Dutch vowel purge, the Spanish consonant-triplet rule…). `Match#strict?` separates validity claims from shape hits: it is true when a hit comes from a series the dataset marks `matching: strict` (the plates spec §2.7), and false for recall-only catch-alls (export plates accept any current shape, by law; Spain's 1900–1971 numeric era is one wide union). Validate user input with `strict?`.
|
|
684
|
+
|
|
685
|
+
When a plate has a series' shape but breaks its alphabet, `near_misses` names that series, so a form can say why:
|
|
686
|
+
|
|
687
|
+
```ruby
|
|
688
|
+
m = Vehicles.plate("1234 ABC", jurisdiction: :es)
|
|
689
|
+
m.valid? # => false
|
|
690
|
+
m.near_misses.map(&:id) # => ["es-national-2000"]
|
|
691
|
+
m.jurisdiction.charset["excluded_letters"] # => ["A", "E", "I", "O", "U", "Ñ", "Q"]
|
|
692
|
+
```
|
|
693
|
+
|
|
694
|
+
One reason code per answer, for the error message (the hosted API answers the same codes):
|
|
695
|
+
|
|
696
|
+
```ruby
|
|
697
|
+
Vehicles.plate("1234 BCD", jurisdiction: :es).reason # => :ok
|
|
698
|
+
Vehicles.plate("1234 ABC", jurisdiction: :es).reason # => :excluded_letters
|
|
699
|
+
Vehicles.plate("1234 ABC", jurisdiction: :es).excluded_letters # => ["A"]
|
|
700
|
+
Vehicles.plate("12345", jurisdiction: :es).reason # => :bad_shape
|
|
701
|
+
# also :unverifiable (only a recall-only series has the shape), :empty, :unknown_jurisdiction
|
|
702
|
+
```
|
|
703
|
+
|
|
704
|
+
Moving people (ride sharing, rentals)? `passenger?` says whether a strictly valid
|
|
705
|
+
plate belongs to a series issued to passenger-carrying vehicles — a trailer
|
|
706
|
+
(`R-1234-BCD`) or tractor plate (`E-1234-BCD`) is valid but not that:
|
|
707
|
+
|
|
708
|
+
```ruby
|
|
709
|
+
Vehicles.plate("1234 BCD", jurisdiction: :es).passenger? # => true
|
|
710
|
+
Vehicles.plate("R-1234-BCD", jurisdiction: :es).passenger? # => false (reason is still :ok)
|
|
711
|
+
Vehicles.plates(:es).series.reject(&:passenger?).map(&:klass) # => ["agricultural", "trailer"]
|
|
712
|
+
```
|
|
713
|
+
|
|
714
|
+
A plate is personal data: the matcher is local, and nothing here sends it anywhere.
|
|
524
715
|
|
|
525
716
|
```ruby
|
|
526
717
|
Vehicles.plates # => every jurisdiction
|
|
527
|
-
Vehicles.plates(:es).series # =>
|
|
718
|
+
Vehicles.plates(:es).series # => 19 series: formats, periods, sourced designs
|
|
528
719
|
```
|
|
529
720
|
|
|
530
721
|
Pilot coverage (gate L0): 🇳🇱 🇪🇸 🇩🇪 + US-Florida, 73 series, corpus-proven in NL. Same posture as everything else here: bundled, offline, forgiving in, honest out.
|
|
@@ -565,21 +756,51 @@ end
|
|
|
565
756
|
|
|
566
757
|
car = Vehicles.find("vw golf")
|
|
567
758
|
|
|
568
|
-
#
|
|
759
|
+
# Studio vehicle imagery — LIVE today. Mint a key at
|
|
569
760
|
# https://vehiclesdb.com/settings/api-keys
|
|
570
761
|
car.image # => "https://vehiclesdb.com/…/md.webp" (640×360)
|
|
571
762
|
car.image(size: :lg) # :sm 320×180 · :md 640×360 · :lg 1280×720
|
|
572
|
-
car.image(color: "red") # color-accurate;
|
|
763
|
+
car.image(color: "red") # color-accurate; unavailable colors fall back honestly
|
|
573
764
|
car.images(color: "red") # the full payload: every variant + dimensions,
|
|
574
|
-
#
|
|
765
|
+
# available palette, served-vs-requested color
|
|
575
766
|
# color slugs are the gem's own canon — Vehicles.colors — so a stored
|
|
576
|
-
# dropdown value maps 1:1 to the
|
|
767
|
+
# dropdown value maps 1:1 to the studio imagery.
|
|
768
|
+
|
|
769
|
+
# Enrichment — the wired-up seam. nil today: the API does not serve these yet.
|
|
770
|
+
car.years # production years (nil until published)
|
|
771
|
+
car.segment # editorial segment (nil until published)
|
|
772
|
+
```
|
|
773
|
+
|
|
774
|
+
**Images in a Rails app.** Every served image is billed, so the gem caches
|
|
775
|
+
whole payloads (in-process by default; share them with `Rails.cache`) and never
|
|
776
|
+
raises. `image` answers nil on any failure; when you need to know **why**, ask
|
|
777
|
+
`images_result`:
|
|
778
|
+
|
|
779
|
+
```ruby
|
|
780
|
+
Vehicles.configure do |config|
|
|
781
|
+
config.image_cache = Rails.cache # a hit is kept a day (capped by the payload's expiry), a 404 ten minutes
|
|
782
|
+
config.on_hosted_error = ->(result) { Rails.error.report(result.to_error, handled: true) }
|
|
783
|
+
end
|
|
577
784
|
|
|
578
|
-
|
|
579
|
-
car.
|
|
580
|
-
car.
|
|
785
|
+
car = Vehicles.by_id(record.vehiclesdb_id)
|
|
786
|
+
car.image(color: record.color, exact_color: true) # nil instead of another color's image
|
|
787
|
+
r = car.images_result(color: "red")
|
|
788
|
+
r.ok? # => false
|
|
789
|
+
r.reason # => :payment_required (:no_api_key :not_found :unauthorized :rate_limited
|
|
790
|
+
# :invalid_request :server_error :timeout :network :color_not_available …)
|
|
791
|
+
r.error # => "insufficient_credits" (the API's own code, verbatim)
|
|
581
792
|
```
|
|
582
793
|
|
|
794
|
+
- `on_hosted_error` (and a warn log line) fires for a bad key, an empty wallet,
|
|
795
|
+
rate limiting, server errors and timeouts — never for a plain "no image yet".
|
|
796
|
+
- A record a dataset release renamed answers with a 301 to its successor; the gem
|
|
797
|
+
follows it (same host only, so your key never leaves it).
|
|
798
|
+
- `year:` is sent to the API, which answers `422 unsupported_filter` until
|
|
799
|
+
year-accurate images ship (`image` → nil, `reason: :invalid_request`); until
|
|
800
|
+
0.9.1 the gem silently dropped it.
|
|
801
|
+
- With `exact_color: true` a model's known palette is remembered, so a color it
|
|
802
|
+
is not available in costs no request at all.
|
|
803
|
+
|
|
583
804
|
Under the hood this is a simple **provider** model: a `LocalProvider` (the bundled data) is always available, and a `HostedProvider` activates only when an API key is configured. Calls prefer the hosted data when it's there and **fall back to the local data otherwise — never raising, never blocking** your request:
|
|
584
805
|
|
|
585
806
|
```ruby
|
|
@@ -599,18 +820,19 @@ Vehicles.configure do |config|
|
|
|
599
820
|
config.region = :eu # default region for queries
|
|
600
821
|
config.api_key = ENV["VEHICLESDB_API_KEY"] # optional hosted VehiclesDB data
|
|
601
822
|
config.aliases = { "Chevy" => "Chevrolet" } # add your own make aliases
|
|
602
|
-
config.use_cache = true #
|
|
823
|
+
config.use_cache = true # serve a refreshed dataset when it is usable
|
|
824
|
+
config.cache_dir = Rails.root.join("storage", "vehicles") # a shared volume for multi-container deploys
|
|
825
|
+
config.image_cache = Rails.cache # hosted image payloads, shared across processes
|
|
603
826
|
end
|
|
604
827
|
```
|
|
605
828
|
|
|
606
829
|
## Staying current (optional)
|
|
607
830
|
|
|
608
831
|
The bundled snapshot works offline forever — but vehicle data changes (new
|
|
609
|
-
makes, fixes), and you shouldn't have to ship a gem upgrade to
|
|
610
|
-
get them. So `vehicles` can **refresh** from the published
|
|
611
|
-
[dataset](https://github.com/vehiclesdb/vehiclesdb) into a
|
|
612
|
-
|
|
613
|
-
a gem bump.**
|
|
832
|
+
makes, fixes, renamed records), and you shouldn't have to ship a gem upgrade to
|
|
833
|
+
every app just to get them. So `vehicles` can **refresh** from the published
|
|
834
|
+
[dataset](https://github.com/vehiclesdb/vehiclesdb) into a file cache. Data
|
|
835
|
+
fixes reach your app **without a gem bump.**
|
|
614
836
|
|
|
615
837
|
`rails g vehicles:install` drops a `VehiclesRefreshJob` — schedule it (daily is
|
|
616
838
|
plenty), e.g. with solid_queue:
|
|
@@ -622,16 +844,58 @@ vehicles_refresh:
|
|
|
622
844
|
schedule: every day at 3am
|
|
623
845
|
```
|
|
624
846
|
|
|
625
|
-
Or refresh
|
|
847
|
+
Or refresh by hand:
|
|
848
|
+
|
|
849
|
+
```ruby
|
|
850
|
+
result = Vehicles.refresh # never raises
|
|
851
|
+
result.status # => :updated | :current | :rejected | :failed
|
|
852
|
+
result.message # => "dataset 2026.11.2 → 2026.12.0"
|
|
853
|
+
Vehicles.refresh! # the same, as true/false (true when updated or already current)
|
|
854
|
+
Vehicles.data_version # => the version now in effect (cached, or bundled)
|
|
855
|
+
```
|
|
856
|
+
|
|
857
|
+
**Safe by construction.** A release is written only when all of these hold;
|
|
858
|
+
otherwise your current data stays untouched and `result.reason` says why:
|
|
859
|
+
|
|
860
|
+
1. The release **manifest** (a few KB, read first) announces a dataset schema
|
|
861
|
+
this gem reads and a version **newer** than the newest data you already have.
|
|
862
|
+
Same version → nothing is downloaded. Older → refused: never a downgrade.
|
|
863
|
+
2. The payload is fetched **from that exact release**, parses, carries that
|
|
864
|
+
version and schema, and matches the manifest's per-kind model counts (and
|
|
865
|
+
its checksum, when the manifest publishes one).
|
|
866
|
+
3. No kind, and no kind-in-a-country (e.g. Spain's cars), shrinks below
|
|
867
|
+
`config.refresh_min_ratio` (0.9) of the data in use — a broken release can
|
|
868
|
+
never empty your pickers.
|
|
869
|
+
4. Your own `config.refresh_guard`, if set, accepts it:
|
|
870
|
+
`->(new_data, current) { … }` with two `Vehicles::Dataset`s.
|
|
871
|
+
|
|
872
|
+
It is written atomically (temp file + rename). And the file a process serves is
|
|
873
|
+
chosen just as carefully: a refreshed cache is used only while it is **not older
|
|
874
|
+
than the gem's bundled data** and in a schema the gem reads — upgrading the gem
|
|
875
|
+
always wins over an older download.
|
|
876
|
+
|
|
877
|
+
**Every process picks it up on its own.** Each process re-checks the file at
|
|
878
|
+
most every `config.reload_check_interval` seconds (default 5; one `File.stat`)
|
|
879
|
+
and switches to a new one — no restart, no hook. A file that is corrupt or
|
|
880
|
+
half-written never replaces the dataset in memory.
|
|
881
|
+
|
|
882
|
+
**Deploying web and jobs as separate containers** (Kamal, Docker Compose,
|
|
883
|
+
Kubernetes)? Give them one shared volume and point the gem at it, so the job's
|
|
884
|
+
refresh reaches the web containers:
|
|
626
885
|
|
|
627
886
|
```ruby
|
|
628
|
-
|
|
629
|
-
Vehicles.
|
|
887
|
+
# config/initializers/vehicles.rb — or set VEHICLES_CACHE_DIR in both containers
|
|
888
|
+
Vehicles.configure { |c| c.cache_dir = Rails.root.join("storage", "vehicles") }
|
|
630
889
|
```
|
|
631
890
|
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
891
|
+
```yaml
|
|
892
|
+
# config/deploy.yml (Kamal): web and job already share the storage volume
|
|
893
|
+
volumes:
|
|
894
|
+
- "myapp_storage:/rails/storage"
|
|
895
|
+
```
|
|
896
|
+
|
|
897
|
+
Run the refresh in ONE place (the job container); every container serves the
|
|
898
|
+
result. Want it fully offline/deterministic instead? `config.use_cache = false`.
|
|
635
899
|
|
|
636
900
|
## The full Ruby API
|
|
637
901
|
|
|
@@ -639,8 +903,10 @@ fully offline/deterministic? `config.use_cache = false`.
|
|
|
639
903
|
# Lists (strings — drop straight into a form)
|
|
640
904
|
Vehicles.makes # => [String]
|
|
641
905
|
Vehicles.makes(kind: :car, region: :eu)
|
|
906
|
+
Vehicles.makes(kind: :car, country: :es) # only makes evidenced in that country
|
|
642
907
|
Vehicles.models("Audi") # => [String]
|
|
643
908
|
Vehicles.models("Audi", body_type: :suv)
|
|
909
|
+
Vehicles.models("SEAT", kind: :car, country: :es) # in Spain's popularity order
|
|
644
910
|
|
|
645
911
|
# Option pairs (for Rails `select`)
|
|
646
912
|
Vehicles.make_options # => [[label, value]]
|
|
@@ -648,14 +914,24 @@ Vehicles.model_options("Audi") # => [[label, value]]
|
|
|
648
914
|
|
|
649
915
|
# Whole make => [models] map (embed once for a client-side dependent picker)
|
|
650
916
|
Vehicles.catalog(kind: :car) # => { "Audi" => ["A3", ...], ... }
|
|
917
|
+
Vehicles.catalog(kind: :car, country: :es) # one country's picker list
|
|
918
|
+
Vehicles.dataset_slice(kind: :car, country: :es) # the same scope as dist/vehicles.json records (widget `data`)
|
|
651
919
|
|
|
652
920
|
# Objects
|
|
653
921
|
Vehicles.make("Audi") # => Vehicles::Make | nil
|
|
654
922
|
Vehicles.find("audi a3") # => Vehicles::Model | nil (one free-text string)
|
|
655
923
|
Vehicles.model("Audi", "A3") # => Vehicles::Model | nil (a stored make+model pair)
|
|
656
|
-
Vehicles.model("
|
|
924
|
+
Vehicles.model("Iveco", "Daily", kind: :bus) # pick a kind (default: the one kind rule)
|
|
925
|
+
Vehicles.model("Iveco", "Daily", kinds: [:car, :van]) # allow only some kinds
|
|
657
926
|
Vehicles.match_model("VW", "Tuareg") # => Vehicles::ModelMatch | nil (model, rung, confidence, via, candidates)
|
|
927
|
+
Vehicles.match_model("VW", "Golf").confident? # => true: safe to auto-apply
|
|
658
928
|
Vehicles.identify("vw tuareg") # => Vehicles::ModelMatch | nil (one free-text string)
|
|
929
|
+
|
|
930
|
+
# Canonical ids
|
|
931
|
+
Vehicles.by_id("car/volkswagen/golf") # => Vehicles::Model | nil (follows former ids)
|
|
932
|
+
Vehicles.by_id(id, follow: false) # exact only
|
|
933
|
+
Vehicles.successor(id) # => the id to store now | nil
|
|
934
|
+
Vehicles.live?(id) # => true when the id names a current record
|
|
659
935
|
Vehicles.search("a3") # => [Vehicles::Model]
|
|
660
936
|
|
|
661
937
|
# Popularity & availability (open data, measured from official registers)
|
|
@@ -672,26 +948,35 @@ model.aliases model.forms # published alternate names; cura
|
|
|
672
948
|
model.kind model.body_type model.suv? model.coupe? # …predicates
|
|
673
949
|
model.global_decile model.popular? # 1 = top 10% worldwide; nil-safe
|
|
674
950
|
model.availability model.available_in?(:nl) model.two_wheeler?
|
|
675
|
-
model.
|
|
951
|
+
model.id model.former_ids # "car/volkswagen/golf"; ids this record replaced
|
|
952
|
+
model.registration_years # { first: 1998, last: 2026 } hint, or nil (never a validation bound)
|
|
953
|
+
model.years model.segment # production years / segment: nil until published
|
|
954
|
+
model.image(color:, size:, exact_color:) model.images(color:) model.images_result(color:) # ← hosted API
|
|
676
955
|
|
|
677
956
|
# Colors (canonical palette)
|
|
678
957
|
Vehicles.colors # => [Vehicles::Color]
|
|
679
958
|
Vehicles.color("navy") # => Vehicles::Color | nil (forgiving)
|
|
680
|
-
Vehicles.color_options
|
|
681
|
-
color.slug color.name color.hex
|
|
959
|
+
Vehicles.color_options(locale: :es) # => [[label, slug]] (for select; :en default)
|
|
960
|
+
color.slug color.name color.hex color.label(:es)
|
|
961
|
+
Vehicles.kind_options(locale: :es) Vehicles.kind_label(:van, locale: :es)
|
|
962
|
+
Vehicles.body_type_options(locale: :es) Vehicles.body_type_label(:wagon, locale: :es)
|
|
963
|
+
|
|
964
|
+
# Plates
|
|
965
|
+
Vehicles.plate("1234 BCD", jurisdiction: :es) # => Plates::Match: valid? strict? reason passenger? suggestion
|
|
682
966
|
|
|
683
967
|
# Meta
|
|
684
968
|
Vehicles.data_version # => "2026.07.0" (version in effect: cached or bundled)
|
|
685
969
|
Vehicles.region # => :global
|
|
686
970
|
|
|
687
971
|
# Refresh (optional — keep data current without a gem upgrade)
|
|
688
|
-
Vehicles.refresh
|
|
689
|
-
Vehicles.
|
|
972
|
+
Vehicles.refresh # => Vehicles::RefreshResult (status, reason, message); never raises
|
|
973
|
+
Vehicles.refresh! # the same as true/false
|
|
974
|
+
Vehicles.reload! # drop the in-memory dataset now (processes also re-check on their own)
|
|
690
975
|
```
|
|
691
976
|
|
|
692
977
|
## Where the data comes from
|
|
693
978
|
|
|
694
|
-
The bundled dataset is [**VehiclesDB**](https://github.com/vehiclesdb/vehiclesdb) — an open dataset **reconciled from official vehicle registers of
|
|
979
|
+
The bundled dataset is [**VehiclesDB**](https://github.com/vehiclesdb/vehiclesdb) — an open dataset **reconciled from the official vehicle registers of every covered country, on five continents** (the Dutch RDW, UK DfT, Spanish DGT, Finnish Traficom, German KBA, US EPA, New Zealand's Motor Vehicle Register, Thailand's DLT, Ukraine's MVS, and more — the exact list for the bundled 2026.11.6 snapshot is `countries` in the data repo's `manifest.json`). A model ships only when **two independent official sources corroborate it** — or one shows a registration count no typo could produce — so you get real nameplates without the registry noise.
|
|
695
980
|
|
|
696
981
|
Every record is shaped like this:
|
|
697
982
|
|
|
@@ -725,15 +1010,14 @@ No magic, just good defaults:
|
|
|
725
1010
|
|
|
726
1011
|
## Roadmap
|
|
727
1012
|
|
|
728
|
-
Bundled today:
|
|
729
|
-
availability, plus the MCP server
|
|
1013
|
+
Bundled today: the full VehiclesDB catalog across all 6 kinds, with popularity +
|
|
1014
|
+
availability, plus the MCP server (the live counts are in the data repo's
|
|
1015
|
+
`manifest.json`; this README deliberately carries none). On the way:
|
|
730
1016
|
|
|
731
|
-
- 🌍 More **countries** (
|
|
1017
|
+
- 🌍 More **countries** (the dataset grows monthly)
|
|
732
1018
|
- 📅 Production **years** in the local data
|
|
733
1019
|
- 🧬 **Generations & variants** layers (the schema already reserves them)
|
|
734
|
-
- 🖼️ Model **images** (via VehiclesDB)
|
|
735
1020
|
- 🏷️ **Segments** (supercar, city car, hot hatch) and richer metadata
|
|
736
|
-
- 🔎 A mountable **autocomplete endpoint** so the dependent-dropdown recipe becomes one line
|
|
737
1021
|
|
|
738
1022
|
Want one of these sooner? Open an issue.
|
|
739
1023
|
|