All notable changes to this project are documented in this file.
The format is based on Keep a Changelog, and this project adheres to Semantic Versioning.
- Plates dataset refreshed — the overnight swarm passes' settlements land: the Cypriot centre date mark, Brazil's square-dot printed form and photographed moto split, San Marino's structured legend/sigla fields, Ukraine's statutory line counts, California's pixel-sampled colours and corner-sticker guides, the Texas exempt restatement, Korea's corporate characters block, Thailand's seven-photo cross-check, and China's two-row trailer + NEV dedicated-mark flips (#288-#290).
Plates.decode_table(name)— the _decode lookups (district/office/ use-mark tables) exposed to consumers, enabling serial-driven subregion resolution (renderers picking the right Land arms from a district prefix).
- South Korea (21 series + hangul decode tables) — corpus: 124 jurisdictions / 1,381 series.
- Pattern-language literal escapes: ca-nb-truck-2003 introduced "\L" (a literal L, vs the any-letter placeholder). New Series#display_pattern resolves escapes for display and slot walks; the round-trip test substitutes placeholders escape-aware. Escape convention flagged to the data maintainers for a ruling.
- The L3 wave: 28 jurisdiction files across five continents (jp cn in th my br mx ar nz za ua + Australia's 8 states + Canada's 13 provinces/territories), 415 series and 10 more decode tables incl. Japan's 138-mark statutory office table. Corpus now: 123 jurisdictions / 1,360 series / 34 decode tables.
- Plates corpus refreshed to 91 jurisdictions (the EU/EEA wave-2 landing: ie dk no is pt lu pl cz sk hu ro si hr ee lv lt gr cy mt + microstates) with 22 decode tables.
- The FULL plates corpus ships in the gem: 67 jurisdictions / 606 series (the 8-country L1 EU wave, all 50 US states + DC + 5 territories, and the redated pilots) + 10 decode tables — up from the 4 pilot files. The art tier stays in the data repo; the gem bundles format data.
- Bundled plates data refreshed from the redate pass: es-provincial is now 1971-2000 pinned to Decreto 2046/1971 at both ends (was a 1999-2000 instrument window), plus the 1900-1971 Spanish numeric era, the German 1956-2000 DIN era, 11 German and 10 Florida period corrections — all cited to gazette primaries.
approximate_start?covers the two new honesty vocabularies (instrument-in-force-upper-bound, secondary-locator-unverified), so upper-bounded and uncited-secondary starts render with the "~".
- Plate period labels read naturally: open series say "2000 → today", and
instrument-dated starts mark approximation as "~1999" instead of the
cryptic "≤1999". New
Series#approximate_start?so displays can explain the tilde ("documented from this year; the format may be older").
Plates::Series#format_serialreconstructed variable-length serials by walking the fixed display pattern, misplacing separators ("M1234LA" came back as "M1-234L-A" instead of "M-1234-LA"). The pattern walk now applies only when the serial exactly fills the pattern; length-mismatched serials reconstruct from the regex segmented at its separators (newSeries.segment_separators, riding the dataset separator contract), and a serial neither path can place comes back untouched rather than mis-punctuated.
Plates::Series#period_labelno longer presents instrument-dated starts as the series era: rows whoseperiod_evidenceisinstrument-in-forceorinstrument-windowlabel as "≤1999–2000" ("began by", not "began in"). NewSeries#period_evidence,#issued_regexp,#issued_separatorsreaders for consumers mirroring the reconstruction.
Model#former_ids— the full canonical ids a record absorbed through the dataset's append-only migration contract (e.g.car/alfa-romeo/159sw). The field was already in the bundled snapshot; the reader now surfaces it, so integrators can migrate stored ids without re-parsing the raw JSON. Also included inModel#to_h.
License plates become a first-class citizen: the PRD-PLATES registration-mark dataset ships in the gem, bundled and offline like everything else.
Vehicles.plate(input, jurisdiction:)— two-tier plate validation: exact as-issued, then separator-forgiving ("1234XYZ" == "1234 XYZ" == "1234-xyz") withMatch#suggestionreturning the as-issued formatting. Leniency forgives punctuation, never the alphabet — strict regexes encode what each authority really issues.Match#strict?separates authority-alphabet hits from recall-only catch-alls (export plates etc.).Vehicles.plates/Vehicles.plates(:nl)— the full series data: 73 series across NL/ES/DE/US-FL (gate L0, NL corpus-proven), each with pattern, period, class, categories, sourced design facts and citations.- Bundled data under
data/plates/(CC-BY, pinned upstream commit indata/plates/PROVENANCE.md).
The hosted images API is live — this release points the wired-up provider seam at the real thing.
Model#image(color:, size:)returns a rendered variant URL from the live VehiclesDB images API (:sm320×180,:md640×360,:lg1280×720, webp). Mint a key at https://vehiclesdb.com/settings/api-keys and setconfig.api_key; without a key it stays nil, as always.Model#images(color:)returns the full payload: every variant with dimensions, the rendered palette for the model, served-vs-requested color (un-rendered colors fall back honestly instead of 404ing), provenance.
config.api_base_urldefault is nowhttps://vehiclesdb.com(the live API origin; endpoint paths carry/v1). It must be an ORIGIN — no path suffix.Model#imageacceptsyear:but does not send it:year/trimare reserved filters server-side (the API 422s on them by contract) until year-accurate renders ship.
0.4.1 - 2026-07-26
Data-only refresh to VehiclesDB dataset 2026.07.4 — the July correction pass: 16,948 models across 858 makes (~1,200 registry-junk and duplicate records removed, per-marque naming canon applied). No API changes.
0.4.0 - 2026-07-09
A first-class "Other / not in the list" escape hatch for make/model pickers, so a vehicle the dataset doesn't cover is never a dead end:
include_other:onmakes,models,make_options, andmodel_optionsappends the option (name lists get the label; option pairs get the stable"other"slug). Idempotent.allow_other: trueon thevehicle_make/vehicle_modelvalidators accepts it, so the dropdown and the validation agree by construction.config.other_label(default"Other") localizes the label;Vehicles.other?recognizes the label or the canonical"other"slug (case/diacritics-forgiving), andVehicles.other_labelreads it back.
0.3.0 - 2026-07-05
Continent filtering, rarity tiers, and alternate names — plus a cleaner dataset (Tesla is now 6 nameplates, not 43; ~470 duplicate makes/models merged).
- Continents:
model.regions([:eu, :as, …]),model.available_in_region?(:eu), and predicate sugar.european?/.asian?/.north_american?/.south_american?/.oceanian?/.african?.region:onmakes/models/top_modelsnow filters by continent;Vehicles.regionslists covered continents;make.continents/make.in_region?. - Rarity tiers:
model.rarity→:common(decile 1–3) /:average(4–7) /:rare(8–10) /:unknown(unranked);model.common?/model.rare?. Filter withVehicles.catalog_slice(kind:, region:, rarity:, max_decile:)andVehicles.models(make, rarity:, max_decile:)— the "how much data do I want to show" knob, without exposing raw counts. - Alternate names:
make.aliases/model.aliasesnow carry documented nicknames, initialisms and native-script names (Chevy, VW, 比亚迪, カローラ, Rabbit, Pajero/Montero/Shogun…). All resolve in lookups —Vehicles.make("比亚迪")andVehicles.make("Chevy")both find the make. model.to_hgainsregions,rarity,aliases.
config.regionnow defaults to nil (no continent filter — the whole global dataset) instead of:eu.region:means a continent everywhere; a country code like:usreturns nothing (usecountry:ontop_modelsfor countries). Bundled snapshot: VehiclesDB2026.07.2.
0.2.0 - 2026-07-05
The global multi-kind release: the bundled dataset grows from ~456 EU car models to 18,556 models across 928 makes and six kinds (cars, motorcycles, mopeds, vans, trucks, buses), reconciled from official registers of 14 countries — see VehiclesDB.
- Popularity:
model.global_decile(1 = top 10% worldwide, measured from official registration counts;nil= unranked) andmodel.popular?(decile ≤ 2, honestfalsewhen unranked). - Availability:
model.availability(ISO alpha-2 country codes with official evidence) andmodel.available_in?(:nl). Evidence of presence, not marketing history — grey imports count. Vehicles.top_models(kind:, country:, limit:)— ranked by popularity, perfect for "common choices" pickers. Unranked models never outrank ranked.Vehicles.kindsandmodel.two_wheeler?(motorcycle OR moped — the union two-wheeler pickers want).vehicles-mcp— a bundled, read-only MCP server over the dataset (stdio, zero config, stdlib-only):search_makes,search_models,get_model,top_models. Wire it into any MCP host with{ "command": "vehicles-mcp" }.model.to_hnow includesglobal_decileandavailability.
- Dataset: bundled snapshot is now VehiclesDB
2026.07.0(global, 6 kinds, popularity + availability).Vehicles.regionreturns:global; the region gate treats a global snapshot as covering every region, soregion: :eucallers keep working unchanged. model.body_typeisnilfor kinds without an honest body vocabulary (a truck is not a "hatchback"); predicates returnfalseonnil. Cars keep their curated body types, unchanged.- Removed the built-in
"vauxhall" → Opellookup alias: the dataset now ships Vauxhall and Opel as separate makes (deliberately — separate GB/EU model names), and the alias would have shadowed the real Vauxhall records.
- Data refresh now handles real HTTP responses correctly.
Net::HTTPreturns an ASCII-8BIT body (e.g. gzip-decompressed); with multibyte UTF-8 data andEncoding.default_internal = UTF-8(as Rails sets), the cache write raisedEncoding::UndefinedConversionErrorand the refresh silently failed (falling back to the bundled snapshot). The fetched body is now tagged UTF-8 and the cache is written withFile.binwrite(no transcoding).Vehicles.refresh!works under Rails.
0.1.0 - 2026-06-23
Initial release.
- Bundled, zero-config dataset of EU car make/model nameplates (~47 makes,
~456 models, dataset version
2026.06.0), derived from RDW Open Data (CC-BY 4.0). - Each model carries a
kind(:cartoday) and a curatedbody_type(:hatchback,:sedan,:suv,:mpv,:coupe,:wagon,:convertible,:roadster,:van). - Core query API:
Vehicles.makes,Vehicles.models,Vehicles.make,Vehicles.find,Vehicles.model(make, model),Vehicles.search, withkind:/body_type:/region:filters. Vehicles.catalog(kind:, region:)— a{ make => [model names] }map, so a dependent make→model picker can be built fully client-side (embed once, no route/controller/fetch). The recommended dropdown recipe in the README.- Data refresh (optional):
Vehicles.refresh!pulls the latest published dataset (from the VehiclesDB data repo via CDN) into a local file cache; loads prefer the cache over the bundled snapshot, so data fixes reach an app without a gem upgrade. Bundled data remains the offline, zero-config floor. Config:data_url/cache_path/use_cache. The install generator drops a schedulableVehiclesRefreshJob. Error-isolated (never raises; a bad download never clobbers good data). - Canonical color palette:
Vehicles.colors,Vehicles.color(query)(forgiving, with synonyms),Vehicles.color_options, and theVehicles::Colorvalue object (slug/name/hex) — shared vocabulary for color dropdowns and future image variants. - Rails dropdown helpers:
Vehicles.make_options,Vehicles.model_options. - Forgiving lookups: case-, diacritic-, slug-, and alias-insensitive
(
"VW","merc","Vauxhall","Škoda"all resolve). Vehicles::MakeandVehicles::Modelvalue objects with predicate sugar (car.suv?,car.hatchback?) andto_h.- Drop-in ActiveModel validators:
vehicle_makeandvehicle_model. - Provider seam (
LocalProvider+HostedProvider) for optional hosted VehiclesDB enrichment (model.years/#segment/#image), gated on an API key and degrading gracefully to local data. - Install generator that writes a configuration initializer (no migration — the gem has no database table).