vehicles 0.4.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 (189) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop_todo.yml +164 -0
  3. data/CHANGELOG.md +321 -0
  4. data/README.md +88 -15
  5. data/data/PROVENANCE-vehicles.md +12 -0
  6. data/data/plates/PROVENANCE.md +10 -0
  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/es-provinces.yml +116 -0
  14. data/data/plates/_decode/fr-departements.yml +191 -0
  15. data/data/plates/_decode/gb-age-identifiers.yml +170 -0
  16. data/data/plates/_decode/gb-memory-tags.yml +202 -0
  17. data/data/plates/_decode/gr-prefectures.yml +1408 -0
  18. data/data/plates/_decode/hr-cities.yml +315 -0
  19. data/data/plates/_decode/ie-counties.yml +227 -0
  20. data/data/plates/_decode/in-state-codes.yml +169 -0
  21. data/data/plates/_decode/is-districts-1938.yml +116 -0
  22. data/data/plates/_decode/it-provinces.yml +348 -0
  23. data/data/plates/_decode/jp-office-marks.yml +960 -0
  24. data/data/plates/_decode/jp-use-marks.yml +205 -0
  25. data/data/plates/_decode/kr-authority-marks.yml +146 -0
  26. data/data/plates/_decode/kr-use-syllables.yml +194 -0
  27. data/data/plates/_decode/lu-series-order.yml +638 -0
  28. data/data/plates/_decode/mx-entity-series.yml +380 -0
  29. data/data/plates/_decode/my-index-marks.yml +271 -0
  30. data/data/plates/_decode/no-counties-1913.yml +148 -0
  31. data/data/plates/_decode/no-districts.yml +901 -0
  32. data/data/plates/_decode/pl-powiats.yml +567 -0
  33. data/data/plates/_decode/pt-trailer-services.yml +96 -0
  34. data/data/plates/_decode/ro-counties.yml +213 -0
  35. data/data/plates/_decode/se-diplomatic.yml +220 -0
  36. data/data/plates/_decode/si-regions.yml +123 -0
  37. data/data/plates/_decode/sk-districts.yml +314 -0
  38. data/data/plates/_decode/th-registration-elements.yml +208 -0
  39. data/data/plates/_decode/ua-regions.yml +393 -0
  40. data/data/plates/_decode/za-licence-marks.yml +215 -0
  41. data/data/plates/_meta/classes.yml +26 -0
  42. data/data/plates/_meta/separators.yml +102 -0
  43. data/data/plates/ad.yml +961 -0
  44. data/data/plates/ar.yml +762 -0
  45. data/data/plates/at.yml +1424 -0
  46. data/data/plates/au-act.yml +472 -0
  47. data/data/plates/au-nsw.yml +282 -0
  48. data/data/plates/au-nt.yml +431 -0
  49. data/data/plates/au-qld.yml +272 -0
  50. data/data/plates/au-sa.yml +248 -0
  51. data/data/plates/au-tas.yml +302 -0
  52. data/data/plates/au-vic.yml +215 -0
  53. data/data/plates/au-wa.yml +309 -0
  54. data/data/plates/be.yml +1840 -0
  55. data/data/plates/br.yml +1152 -0
  56. data/data/plates/ca-ab.yml +430 -0
  57. data/data/plates/ca-bc.yml +697 -0
  58. data/data/plates/ca-mb.yml +499 -0
  59. data/data/plates/ca-nb.yml +537 -0
  60. data/data/plates/ca-nl.yml +743 -0
  61. data/data/plates/ca-ns.yml +808 -0
  62. data/data/plates/ca-nt.yml +610 -0
  63. data/data/plates/ca-nu.yml +280 -0
  64. data/data/plates/ca-on.yml +881 -0
  65. data/data/plates/ca-pe.yml +592 -0
  66. data/data/plates/ca-qc.yml +1036 -0
  67. data/data/plates/ca-sk.yml +256 -0
  68. data/data/plates/ca-yt.yml +656 -0
  69. data/data/plates/ch.yml +1273 -0
  70. data/data/plates/cn.yml +1431 -0
  71. data/data/plates/cy.yml +688 -0
  72. data/data/plates/cz.yml +1179 -0
  73. data/data/plates/de.yml +1107 -0
  74. data/data/plates/dk.yml +1647 -0
  75. data/data/plates/ee.yml +1275 -0
  76. data/data/plates/es.yml +888 -0
  77. data/data/plates/fi.yml +1271 -0
  78. data/data/plates/fr.yml +1265 -0
  79. data/data/plates/gb.yml +1188 -0
  80. data/data/plates/gr.yml +721 -0
  81. data/data/plates/hr.yml +860 -0
  82. data/data/plates/hu.yml +959 -0
  83. data/data/plates/ie.yml +775 -0
  84. data/data/plates/in.yml +1172 -0
  85. data/data/plates/is.yml +667 -0
  86. data/data/plates/it.yml +1317 -0
  87. data/data/plates/jp.yml +3650 -0
  88. data/data/plates/kr.yml +1375 -0
  89. data/data/plates/li.yml +1010 -0
  90. data/data/plates/lt.yml +879 -0
  91. data/data/plates/lu.yml +842 -0
  92. data/data/plates/lv.yml +924 -0
  93. data/data/plates/mc.yml +1024 -0
  94. data/data/plates/mt.yml +854 -0
  95. data/data/plates/mx.yml +1372 -0
  96. data/data/plates/my.yml +685 -0
  97. data/data/plates/nl.yml +1016 -0
  98. data/data/plates/no.yml +1018 -0
  99. data/data/plates/nz.yml +1699 -0
  100. data/data/plates/pl.yml +1296 -0
  101. data/data/plates/pt.yml +847 -0
  102. data/data/plates/ro.yml +785 -0
  103. data/data/plates/se.yml +1091 -0
  104. data/data/plates/si.yml +1105 -0
  105. data/data/plates/sk.yml +1439 -0
  106. data/data/plates/sm.yml +1119 -0
  107. data/data/plates/th.yml +1150 -0
  108. data/data/plates/ua.yml +1672 -0
  109. data/data/plates/us-ak.yml +763 -0
  110. data/data/plates/us-al.yml +457 -0
  111. data/data/plates/us-ar.yml +427 -0
  112. data/data/plates/us-as.yml +454 -0
  113. data/data/plates/us-az.yml +430 -0
  114. data/data/plates/us-ca.yml +892 -0
  115. data/data/plates/us-co.yml +452 -0
  116. data/data/plates/us-ct.yml +427 -0
  117. data/data/plates/us-dc.yml +614 -0
  118. data/data/plates/us-de.yml +551 -0
  119. data/data/plates/us-fl.yml +741 -0
  120. data/data/plates/us-ga.yml +644 -0
  121. data/data/plates/us-gu.yml +658 -0
  122. data/data/plates/us-hi.yml +585 -0
  123. data/data/plates/us-ia.yml +438 -0
  124. data/data/plates/us-id.yml +531 -0
  125. data/data/plates/us-il.yml +516 -0
  126. data/data/plates/us-in.yml +324 -0
  127. data/data/plates/us-ks.yml +351 -0
  128. data/data/plates/us-ky.yml +770 -0
  129. data/data/plates/us-la.yml +568 -0
  130. data/data/plates/us-ma.yml +379 -0
  131. data/data/plates/us-md.yml +536 -0
  132. data/data/plates/us-me.yml +445 -0
  133. data/data/plates/us-mi.yml +408 -0
  134. data/data/plates/us-mn.yml +411 -0
  135. data/data/plates/us-mo.yml +356 -0
  136. data/data/plates/us-mp.yml +402 -0
  137. data/data/plates/us-ms.yml +432 -0
  138. data/data/plates/us-mt.yml +619 -0
  139. data/data/plates/us-nc.yml +602 -0
  140. data/data/plates/us-nd.yml +419 -0
  141. data/data/plates/us-ne.yml +483 -0
  142. data/data/plates/us-nh.yml +394 -0
  143. data/data/plates/us-nj.yml +547 -0
  144. data/data/plates/us-nm.yml +574 -0
  145. data/data/plates/us-nv.yml +655 -0
  146. data/data/plates/us-ny.yml +592 -0
  147. data/data/plates/us-oh.yml +404 -0
  148. data/data/plates/us-ok.yml +513 -0
  149. data/data/plates/us-or.yml +392 -0
  150. data/data/plates/us-pa.yml +506 -0
  151. data/data/plates/us-pr.yml +507 -0
  152. data/data/plates/us-ri.yml +445 -0
  153. data/data/plates/us-sc.yml +605 -0
  154. data/data/plates/us-sd.yml +428 -0
  155. data/data/plates/us-tn.yml +471 -0
  156. data/data/plates/us-tx.yml +535 -0
  157. data/data/plates/us-ut.yml +384 -0
  158. data/data/plates/us-va.yml +567 -0
  159. data/data/plates/us-vi.yml +489 -0
  160. data/data/plates/us-vt.yml +458 -0
  161. data/data/plates/us-wa.yml +484 -0
  162. data/data/plates/us-wi.yml +509 -0
  163. data/data/plates/us-wv.yml +446 -0
  164. data/data/plates/us-wy.yml +523 -0
  165. data/data/plates/va.yml +604 -0
  166. data/data/plates/za.yml +1141 -0
  167. data/data/vehicles.json +160653 -105090
  168. data/lib/vehicles/configuration.rb +18 -2
  169. data/lib/vehicles/dataset.rb +73 -18
  170. data/lib/vehicles/fuzzy.rb +115 -0
  171. data/lib/vehicles/http.rb +48 -0
  172. data/lib/vehicles/make.rb +36 -6
  173. data/lib/vehicles/mcp_server.rb +5 -5
  174. data/lib/vehicles/model.rb +42 -6
  175. data/lib/vehicles/plates/jurisdiction.rb +32 -0
  176. data/lib/vehicles/plates/series.rb +291 -0
  177. data/lib/vehicles/plates/template_fill.rb +188 -0
  178. data/lib/vehicles/plates/templates.rb +162 -0
  179. data/lib/vehicles/plates.rb +122 -0
  180. data/lib/vehicles/providers/hosted_provider.rb +49 -58
  181. data/lib/vehicles/providers/local_provider.rb +4 -3
  182. data/lib/vehicles/validators/vehicle_make_validator.rb +3 -1
  183. data/lib/vehicles/validators/vehicle_model_validator.rb +1 -1
  184. data/lib/vehicles/version.rb +1 -1
  185. data/lib/vehicles.rb +53 -16
  186. data/rakelib/data_refresh.rake +109 -0
  187. data/rakelib/plates_refresh.rake +29 -0
  188. data/sig/vehicles.rbs +18 -0
  189. metadata +174 -2
data/README.md CHANGED
@@ -5,7 +5,7 @@
5
5
  > [!TIP]
6
6
  > **🚀 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
7
 
8
- `vehicles` gives your Rails app a clean, curated list of vehicle makes and models — **~17k models across 850+ makes: cars, motorcycles, mopeds, vans, trucks and buses**, reconciled from official registers of 14 countries — 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.)
8
+ `vehicles` gives your Rails app a clean, curated list of vehicle makes and models — **~15k models across 850+ makes: cars, motorcycles, mopeds, vans, trucks and buses**, reconciled from official registers of 15 countries — 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
9
 
10
10
  ✨ 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
11
 
@@ -75,12 +75,15 @@ Vehicles.models("toyota") # case-insensitive
75
75
  Vehicles.models("VW") # => Volkswagen (common abbreviation)
76
76
  Vehicles.models("merc") # => Mercedes-Benz
77
77
  Vehicles.models("alfa-romeo") # slug form
78
+ Vehicles.models("marcedes") # => Mercedes-Benz (typo: one make is clearly closest)
78
79
  ```
79
80
 
81
+ Typos resolve too: when nothing matches exactly, the closest make within a small edit distance wins (1 edit up to 5 characters, 2 from 6 up, none under 4) — but only if ONE make is closest, so `"Acra"` (one edit from both Acura and Acma) stays `nil` rather than guessing. Pass `fuzzy: false` (`Vehicles.make("marcedes", fuzzy: false) # => nil`) when you need exact-only matching; the validators always do, so a typo never validates as a stored make. Three more guards keep it from guessing: a swap of two neighbouring letters counts as one edit (`"telsa"` is equally close to Tesla and Temsa, so it stays `nil`); a match may not drop a whole word (so `"Volvo V 60"` never turns into Volvo "60"); and every word keeps its first letter (so `"citroen e"` is not Citroën DS). Your configured `other_label` never fuzzy-matches a real make. Machine input never fuzzes either: Symbols, anything with `/`, `_`, `+` or `\` (ids like `car/mercedes-benz/c-class`, paths like `/makes/marcedes`), and single-case hyphenated slugs (`marcedes-benz`, `MARCEDES-BENZ`) resolve exactly or return `nil` — so routes and stored ids can never land on a guessed make. The MCP server tools are exact-only.
82
+
80
83
  Unknown make? You get an empty array, never an exception:
81
84
 
82
85
  ```ruby
83
- Vehicles.models("DeLorean") # => []
86
+ Vehicles.models("Zzyzx Motors") # => []
84
87
  ```
85
88
 
86
89
  ## Smart lookup & search
@@ -179,8 +182,18 @@ car.popular? # => true decile ≤ 2 (and `false` when unranked —
179
182
  car.availability # => ["es", "fi", "gb", ...] countries with official evidence
180
183
  car.available_in?(:th) # => true
181
184
 
182
- # The most popular models — perfect for "common choices" pre-filled pickers:
183
- Vehicles.top_models(kind: :car, country: :nl, limit: 10) # => [Model, ...]
185
+ # The most popular models — perfect for "common choices" pre-filled pickers.
186
+ # With `country:`, ordered by THAT country's own registration rank (ranks are per kind,
187
+ # so pass `kind:`; countries whose sources record presence only — us, ca for cars —
188
+ # have no ranks and keep the global order):
189
+ Vehicles.top_models(kind: :car, country: :gb, limit: 3).map(&:name) # => ["Fiesta", "Golf", "Corsa"]
190
+ Vehicles.find("vw golf").rank_in(:gb) # => 2 rank among all GB cars (nil where unranked)
191
+
192
+ # Two different "popularity" questions — pick the one you mean:
193
+ car.global_decile # presence average: "popular in MANY countries" (equal country weight)
194
+ car.mass_decile # registration mass per kind: "how many EXIST" (1 = heaviest 10%)
195
+ # nil until your data is 2026.10.1+ (refresh! or a newer gem)
196
+ Vehicles.top_models(kind: :car, by: :mass_decile, limit: 10) # [] on older snapshots
184
197
  Vehicles.top_models(kind: :motorcycle, limit: 10).map(&:full_name)
185
198
  ```
186
199
 
@@ -303,7 +316,7 @@ They're forgiving the same way the lookups are (aliases, case, slugs), and they
303
316
 
304
317
  ### An "Other / not in the list" escape hatch
305
318
 
306
- Make/model dropdowns should never be a dead end — ~17k models is a lot, but
319
+ Make/model dropdowns should never be a dead end — ~15k models is a lot, but
307
320
  someone always shows up with the one car that isn't there. Turn on a first-class
308
321
  "Other" option and the pieces agree by construction: `include_other:` adds it to
309
322
  the picker, `allow_other:` lets it through validation.
@@ -398,6 +411,12 @@ car&.body_type # => :hatchback
398
411
  car&.suv? # => false
399
412
  ```
400
413
 
414
+ It reads what people actually type: case, accents and spacing don't matter
415
+ (`"XCeed"` is Kia's `"X Ceed"`, `"A 5"` is `"A5"`), BMW's Spanish/Italian
416
+ `"Serie 3"` is the `"3 Series"`, and a name that exists as several kinds
417
+ (Toyota's Land Cruiser: car, van, truck, bus) answers with the car unless you
418
+ pass `kind:` — `Vehicles.model("Toyota", "Land Cruiser", kind: :bus)`.
419
+
401
420
  **Only accept the kinds you support.** Filter every list/lookup by `kind:` so
402
421
  the picker only ever offers vehicles your app handles — and widening later (when
403
422
  new kind packs ship, or when you decide to accept them) is a one-word change, not
@@ -422,8 +441,8 @@ Vehicles.find("vw golf").available_in?(:nz) # => true
422
441
  Vehicles.top_models(kind: :car, country: :es) # country-scoped popularity
423
442
  ```
424
443
 
425
- The bundled snapshot is **global** (official registers of 14 countries across
426
- Europe, North America, SE Asia, Oceania and Ukraine — see [Where the data
444
+ The bundled snapshot is **global** (official registers of 15 countries across
445
+ Europe, North America, South America, SE Asia, Oceania and Ukraine — see [Where the data
427
446
  comes from](#where-the-data-comes-from)), so the region gate accepts every
428
447
  region query — `region: :eu` callers from 0.1.x keep working unchanged:
429
448
 
@@ -432,6 +451,50 @@ Vehicles.makes(region: :eu) # fine — a global snapshot covers the EU
432
451
  Vehicles.region # => :global
433
452
  ```
434
453
 
454
+ ## License plates
455
+
456
+ The gem bundles the VehiclesDB **registration-mark dataset** (PRD-PLATES): 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:
457
+
458
+ ```ruby
459
+ Vehicles.plate("12-GB-BD", jurisdiction: :nl).valid? # => true — exact, as issued
460
+ Vehicles.plate("12gbbd", jurisdiction: :nl).suggestion # => "12-GB-BD" (did you mean)
461
+ Vehicles.plate("12-AB-CD", jurisdiction: :nl).strict? # => false — vowels aren't issued
462
+ ```
463
+
464
+ 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 authority-alphabet hits from recall-only catch-alls (export plates accept any current shape, by law).
465
+
466
+ ```ruby
467
+ Vehicles.plates # => every jurisdiction
468
+ Vehicles.plates(:es).series # => 18 series: formats, periods, sourced designs
469
+ ```
470
+
471
+ Pilot coverage (gate L0): 🇳🇱 🇪🇸 🇩🇪 + US-Florida, 73 series, corpus-proven in NL. Same posture as everything else here: bundled, offline, forgiving in, honest out.
472
+
473
+ **Prefer HTTP?** The same matcher and the full format corpus are hosted endpoints on the [VehiclesDB API](https://vehiclesdb.com/api): `GET /api/v1/plates/{code}` (a jurisdiction's every series, with 1:1 SVG render URLs) and `GET /api/v1/plates/{code}/match?plate=…` (validity + as-issued formatting + a render of *that* registration on the real plate design). Free API keys at [vehiclesdb.com](https://vehiclesdb.com).
474
+
475
+ ### Drawing a plate without sending it anywhere
476
+
477
+ A registration is personal data. To show a user their own plate on the real VehiclesDB plate art, the gem fetches a **blank template** (the series, the registration's *shape* — `NNNN LLL`, character classes only — and an optional region) and fills it **locally**:
478
+
479
+ ```ruby
480
+ Vehicles::Plates.render("1234 BCD", jurisdiction: :es)
481
+ # => "<svg … aria-label=\"Spain plate 1234 BCD\">…</svg>" (nil when it can't)
482
+
483
+ Vehicles::Plates.render("1234 BCD", jurisdiction: :es, label: "Matrícula 1234 BCD") # your language
484
+ Vehicles::Plates.render("AB-123-CD", jurisdiction: :fr, region: "75") # FR département band
485
+ ```
486
+
487
+ Only `GET {api_base_url}/plates/es/es-national-2000/template.svg?shape=NNNN_LLL` goes on the wire: no registration, no API key (the route is public). Anything ambiguous (a German `MAB1234` could be `M-AB` or `MA-B`) or unreachable answers `nil` — keep drawing whatever you drew before. Templates are cached in-process; share them across processes with any store that has `read` / `write(key, value, expires_in:)`:
488
+
489
+ ```ruby
490
+ Vehicles.configure do |config|
491
+ config.plate_template_cache = Rails.cache
492
+ config.plate_template_ttl = 7 * 24 * 3600 # templates are public and change only with the plate art
493
+ end
494
+ ```
495
+
496
+ The pieces are public too: `Vehicles::Plates.template(code:, series:, shape:, region:)` returns the blank SVG, and `Vehicles::Plates::TemplateFill.fill(svg, registration)` is the dependency-free filler (stdlib `json` only) if you fetch templates yourself — in a browser, say.
497
+
435
498
  ## 🔓 More with VehiclesDB
436
499
 
437
500
  `vehicles` is the free, open-source SDK for [**VehiclesDB**](https://vehiclesdb.com) — a hosted API for richer vehicle data. The gem is **fully standalone and always will be**; pointing it at VehiclesDB is purely additive. Drop in an API key and the same objects you already use light up with more:
@@ -442,11 +505,20 @@ Vehicles.configure do |config|
442
505
  end
443
506
 
444
507
  car = Vehicles.find("vw golf")
445
- car.years # => 1974..2024 production years
446
- car.segment # => :hatchback / :hot_hatch editorial segment
447
- car.image # => "https://cdn.vehiclesdb.com/volkswagen/golf.webp"
448
- car.image(year: 2020) # year-accurate photo
449
- car.image(year: 2020, color: :silver)
508
+
509
+ # Rendered vehicle imagery — LIVE today. Mint a key at
510
+ # https://vehiclesdb.com/settings/api-keys
511
+ car.image # => "https://vehiclesdb.com/…/md.webp" (640×360)
512
+ car.image(size: :lg) # :sm 320×180 · :md 640×360 · :lg 1280×720
513
+ car.image(color: "red") # color-accurate; un-rendered colors fall back honestly
514
+ car.images(color: "red") # the full payload: every variant + dimensions,
515
+ # rendered palette, served-vs-requested color
516
+ # color slugs are the gem's own canon — Vehicles.colors — so a stored
517
+ # dropdown value maps 1:1 to the rendered imagery.
518
+
519
+ # Enrichment — the wired-up seam; ships next. nil until then, same objects.
520
+ car.years # => 1974..2024 production years
521
+ car.segment # => :hatchback / :hot_hatch editorial segment
450
522
  ```
451
523
 
452
524
  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:
@@ -522,6 +594,7 @@ Vehicles.catalog(kind: :car) # => { "Audi" => ["A3", ...], ... }
522
594
  Vehicles.make("Audi") # => Vehicles::Make | nil
523
595
  Vehicles.find("audi a3") # => Vehicles::Model | nil (one free-text string)
524
596
  Vehicles.model("Audi", "A3") # => Vehicles::Model | nil (a stored make+model pair)
597
+ Vehicles.model("Toyota", "Land Cruiser", kind: :bus) # pick a kind (default: the car)
525
598
  Vehicles.search("a3") # => [Vehicles::Model]
526
599
 
527
600
  # Popularity & availability (open data, measured from official registers)
@@ -556,7 +629,7 @@ Vehicles.reload! # drop the in-memory dataset (reload fro
556
629
 
557
630
  ## Where the data comes from
558
631
 
559
- The bundled dataset is [**VehiclesDB**](https://github.com/vehiclesdb/vehiclesdb) — an open dataset **reconciled from official vehicle registers of 14 countries on 4 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). 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.
632
+ The bundled dataset is [**VehiclesDB**](https://github.com/vehiclesdb/vehiclesdb) — an open dataset **reconciled from official vehicle registers of 15 countries on 5 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.10.0 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.
560
633
 
561
634
  Every record is shaped like this:
562
635
 
@@ -574,7 +647,7 @@ Every record is shaped like this:
574
647
  - **Nameplate-level, on purpose.** "Golf" covers the GTI, the Variant, the R — that's what a dropdown wants. Two-wheelers keep displacement granularity ("Wave110i", "CBR600RR") because that's how riders and registers both speak. Per-trim and per-generation detail is a VehiclesDB API concern.
575
648
  - **Popularity is measured**, from real registration/fleet counts — never scraped rankings. **Availability is evidence** (a country appears because an official source shows the model there), not marketing history.
576
649
  - **Source data:** openly-licensed government sources only, every license pinned and verified at build time — [full source list](https://github.com/vehiclesdb/vehiclesdb/blob/main/SOURCES.md) and [attribution notices](https://github.com/vehiclesdb/vehiclesdb/blob/main/ATTRIBUTION.md).
577
- - **This dataset** (`data/vehicles.json`): **CC-BY 4.0**. Attribution: *"Vehicle data by VehiclesDB (CC-BY 4.0), built from official public registers."*
650
+ - **This dataset** (`data/vehicles.json`): **CC-BY 4.0**. Visible credit with a link is a condition of the license (§3(a)). The form we request: *"Vehicle data by [VehiclesDB](https://vehiclesdb.com)"* — on websites, a followable link (deep links to the make/model page you used are even better); in apps, name us in an about/credits screen. Ready-made snippets (HTML, Markdown, BibTeX, badge): [vehiclesdb.com/attribution](https://vehiclesdb.com/attribution). Prefer not to credit, or need the enriched paid layer? [Commercial license](https://vehiclesdb.com) — it waives only OUR credit, never the upstream register notices.
578
651
  - **The gem code:** MIT.
579
652
 
580
653
  The data is versioned (`Vehicles.data_version`). Each gem release bundles a known snapshot — the offline, deterministic floor. To get newer data, either upgrade the gem or enable [refresh](#staying-current-optional) (which pulls published releases without a gem bump). Either way it's explicit and versioned — no silent mutations.
@@ -590,7 +663,7 @@ No magic, just good defaults:
590
663
 
591
664
  ## Roadmap
592
665
 
593
- Bundled today: 19k models across 6 kinds and 14 countries, with popularity +
666
+ Bundled today: ~15k models across 6 kinds and 15 countries, with popularity +
594
667
  availability, plus the MCP server. On the way:
595
668
 
596
669
  - 🌍 More **countries** (Switzerland next; the dataset grows monthly)
@@ -0,0 +1,12 @@
1
+ # Bundled vehicles dataset
2
+
3
+ `data/vehicles.json` is the public data repo's `dist/vehicles.json`
4
+ (the gem projection) at a release tag:
5
+
6
+ - **Source**: https://github.com/vehiclesdb/vehiclesdb `dist/vehicles.json`
7
+ - **Tag**: `v2026.10.0` (commit `794c27b1881fe561182ff2ee797683cc96047f20`), dataset version `2026.10.0`
8
+ - **country_ranks**: 0 model(s) carried it natively; 14880 merged from
9
+ the same tag's `catalog/<kind>/models.json` `popularity.by_country[cc].rank`
10
+ - **License**: CC-BY 4.0 (see the data repo's LICENSE + ATTRIBUTION.md)
11
+
12
+ Refresh: `rake "data:refresh[<tag>]"` (this file is generated by it).
@@ -0,0 +1,10 @@
1
+ # Bundled plates dataset (PRD-PLATES, full corpus)
2
+
3
+ Copy of `plates/` from the public data repo (EXCLUDING `_art/`),
4
+ refreshed on gem releases:
5
+
6
+ - **Source**: https://github.com/vehiclesdb/vehiclesdb `plates/`
7
+ - **Commit**: `c7dd65c6f4144d39c62706b7be10231a12bc27e1`
8
+ - **License**: CC-BY 4.0 (see the data repo's LICENSE + ATTRIBUTION.md)
9
+
10
+ Refresh: `rake plates:refresh` (this file is generated by it).
@@ -0,0 +1,287 @@
1
+ # plates/_decode/at-districts.yml — Austrian Behördenbezeichnungen im Kennzeichen.
2
+ #
3
+ # THE SOURCE FINDING, and it is the opposite of Germany's: the Austrian
4
+ # district code list IS IN THE REGULATION. § 48 Abs. 5 KFG 1967 delegates it
5
+ # ("Durch Verordnung sind die Bezeichnung der Behörde und die sachlichen
6
+ # Bereiche, das System der Zeichen, unter denen die Fahrzeuge bei der Behörde
7
+ # vorzumerken sind (Abs. 4) ... festzusetzen"), and § 26 Abs. 1 KDV 1967
8
+ # discharges the delegation by pointing at ANLAGE 5d: "Die Bezeichnung der den
9
+ # Zulassungsschein ausstellenden Behörde im Kennzeichen bestimmt sich nach
10
+ # Anlage 5d." Anlage 5d is a CLOSED, PRIMARY, 98-row table printed inside the
11
+ # regulation — so unlike the German ~400-code Bundesanzeiger list (PRD-PLATES
12
+ # §2.4), it is authored here in full, verbatim, in the annex's own order
13
+ # (by Bundesland, then as the annex prints them).
14
+ #
15
+ # The non-geographic prefixes are NOT in Anlage 5d — they are in § 26 KDV
16
+ # itself (Abs. 2 the federal letter A, Abs. 3 the nine Land letters, Abs. 4 the
17
+ # seven sachliche Bereiche, Abs. 5 the diplomatic D and consular K suffixes).
18
+ # They are recorded below in their own blocks, because a parse API needs all
19
+ # four sets to resolve a prefix and because several of them COLLIDE with
20
+ # Anlage 5d district codes (see `collisions`).
21
+ #
22
+ # Referenced by: plates/at.yml (at-standard, at-motorcycle, at-moped,
23
+ # at-historic, at-electric, at-dealer-probefahrt, at-temporary-*,
24
+ # at-trailer-foreign, at-wunschkennzeichen) via region_encoding.
25
+ #
26
+ # PERIOD DISCIPLINE (PRD-PLATES §2.4): these are the codes in the CONSOLIDATED
27
+ # Anlage 5d in force on the access date (Fassung im RIS seit 26.09.2024,
28
+ # NOR40265416). Austrian district reform has repeatedly merged Bezirke
29
+ # (Bruck-Mürzzuschlag, Hartberg-Fürstenfeld, Murtal and Südoststeiermark are
30
+ # themselves 2012/2013 merger codes), and codes withdrawn from the annex stay
31
+ # on the road because a plate is only exchanged on re-registration. WITHDRAWN
32
+ # CODES ARE THEREFORE NOT IN THIS TABLE AND ARE A RECORDED GAP for L4 — a
33
+ # decode of a physical plate whose prefix is absent here must be treated as
34
+ # unresolved, never as invalid.
35
+ jurisdiction: at
36
+ topic: districts
37
+ authority:
38
+ name: "Kraftfahrgesetz-Durchführungsverordnung 1967 (KDV 1967), Anlage 5d (zu § 26 Abs. 1)"
39
+ url: "https://www.ris.bka.gv.at/NormDokument.wxe?Abfrage=Bundesnormen&Gesetzesnummer=10011385&Anlage=5d"
40
+ sources:
41
+ - "https://www.ris.bka.gv.at/NormDokument.wxe?Abfrage=Bundesnormen&Gesetzesnummer=10011385&Anlage=5d # Anlage 5d 'Bezeichnung der Behörde im Kennzeichen', all 98 rows in nine Bundesland blocks; Dokumentnummer NOR40265416, im RIS seit 26.09.2024. Accessed 2026-08-02"
42
+ - "https://www.ris.bka.gv.at/NormDokument.wxe?Abfrage=Bundesnormen&Gesetzesnummer=10011385&Paragraf=26 # § 26 KDV 1967: Abs. 1 (delegation to Anlage 5d), Abs. 2 (letter A), Abs. 3 (nine Land letters), Abs. 4 (sachliche Bereiche BP/FV/PT/BD/BH/JW/FW), Abs. 5 (diplomatic D / consular K, and the Graz exception), Abs. 6 (the Vormerkzeichen grammar), Abs. 7 (hyphen for the Wappen in writing). Accessed 2026-08-02"
43
+ - "https://www.ris.bka.gv.at/NormDokument.wxe?Abfrage=Bundesnormen&Gesetzesnummer=10011384&Paragraf=48 # § 48 Abs. 4 KFG 1967: the prefix is 'einem oder zwei Buchstaben als Bezeichnung der Behörde, in deren örtlichem Wirkungsbereich das Fahrzeug zugelassen ist'; Abs. 5 the delegation. Accessed 2026-08-02"
44
+ period: { start: 1989 }
45
+ period_evidence: enacted-commencement
46
+ period_note: >-
47
+ 1989 is the commencement of the CODE SYSTEM these rows belong to, not of the
48
+ individual rows: BGBl. Nr. 375/1988 (11. KFG-Novelle) Artikel V Abs. 4 lit. b
49
+ put the rewritten § 48 Abs. 4 KFG (one or two letters for the AUTHORITY, in
50
+ place of the pre-1989 single BUNDESLAND letter) into force on 1 January 1989.
51
+ Individual rows are younger or older than that; per-row periods are a
52
+ recorded gap.
53
+
54
+ # A parse API must carry these, because Austrian prefixes are NOT
55
+ # self-delimiting the way German ones are.
56
+ maximal_munch: true
57
+ maximal_munch_note: >-
58
+ Take the LONGEST matching code. The split point is unambiguous for ORDINARY
59
+ registrations because § 26 Abs. 6 Z 2 lit. e KDV requires the Vormerkzeichen
60
+ to BEGIN WITH A DIGIT — so the prefix is exactly the leading letter run. It is
61
+ genuinely AMBIGUOUS for WUNSCHKENNZEICHEN, whose Vormerkzeichen must begin
62
+ with a LETTER (Z 3 lit. e): "BZAB1" is both B (Bregenz) + "ZAB1" and BZ
63
+ (Bludenz) + "AB1", and nothing in the string resolves it. Record the
64
+ ambiguity; do not average it away.
65
+
66
+ collisions:
67
+ - "B — BH Bregenz (Anlage 5d, Vorarlberg) AND the Burgenland Land letter (§ 26 Abs. 3)"
68
+ - "K — LPD Kärnten/Klagenfurt (Anlage 5d) AND the Kärnten Land letter (§ 26 Abs. 3) AND the consular suffix letter (§ 26 Abs. 5 lit. b)"
69
+ - "S — LPD Salzburg/Salzburg (Anlage 5d) AND the Salzburg Land letter (§ 26 Abs. 3)"
70
+ - "W — Landespolizeidirektion Wien (Anlage 5d) AND the Wien Land letter (§ 26 Abs. 3)"
71
+ - "G — LPD Steiermark/Graz (Anlage 5d) AND, per the § 26 Abs. 5 Graz exception, the base of the diplomatic prefix GD and consular prefix GK"
72
+ - "BD — Bundesbus sachlicher Bereich (§ 26 Abs. 4 lit. f) AND the Burgenland diplomatic prefix B+D (§ 26 Abs. 5 lit. a)"
73
+ - "ND — BH Neusiedl am See (Anlage 5d) AND the Niederösterreich diplomatic prefix N+D"
74
+ - "SD — BH Schärding (Anlage 5d) AND the Salzburg diplomatic prefix S+D"
75
+ - "GD — BH Gmünd (Anlage 5d) AND the Graz diplomatic prefix G+D"
76
+ - "NK — BH Neunkirchen (Anlage 5d) AND the Niederösterreich consular prefix N+K"
77
+ - "VK — BH Völkermarkt (Anlage 5d) AND the Vorarlberg consular prefix V+K"
78
+ - "BH — Heeresfahrzeuge (§ 26 Abs. 4 lit. g) AND, in the annex's own prose, the abbreviation for Bezirkshauptmannschaft (which is NOT a code) AND an expressly forbidden Wunschkennzeichen combination (§ 26 Abs. 8 Z 2, 'Blood and Honour')"
79
+ collisions_resolution: >-
80
+ The diplomatic/consular collisions resolve on the SERIAL, not the prefix:
81
+ § 26 Abs. 6 Z 2 lit. d and e require an ordinary Vormerkzeichen to contain one
82
+ to three letters and to END WITH A LETTER, whereas the Abs. 5
83
+ (diplomatic/consular) Vormerkzeichen need only begin with a digit and is in
84
+ practice digits-only — so "ND-12345" is Niederösterreich diplomatic and
85
+ "ND-123XY" is Neusiedl am See. Where an Abs. 5 Vormerkzeichen does carry a
86
+ trailing letter block the collision is UNRESOLVABLE from the string alone.
87
+
88
+ # ---------------------------------------------------------------------------
89
+ # ANLAGE 5d — the 98 authority codes, verbatim, in the annex's own order.
90
+ # `authority` is the annex's own wording, abbreviations expanded once:
91
+ # BH. = Bezirkshauptmannschaft, Mag. = Magistrat, Exp. = Expositur,
92
+ # LPD = Landespolizeidirektion.
93
+ # ---------------------------------------------------------------------------
94
+ codes:
95
+ # I. BURGENLAND
96
+ - { code: E, land: Burgenland, authority: "Landespolizeidirektion Burgenland für das Gebiet der Gemeinden Eisenstadt und Rust", landeshauptstadt: true }
97
+ - { code: EU, land: Burgenland, authority: "BH. Eisenstadt" }
98
+ - { code: ND, land: Burgenland, authority: "BH. Neusiedl am See" }
99
+ - { code: MA, land: Burgenland, authority: "BH. Mattersburg" }
100
+ - { code: OP, land: Burgenland, authority: "BH. Oberpullendorf" }
101
+ - { code: OW, land: Burgenland, authority: "BH. Oberwart" }
102
+ - { code: GS, land: Burgenland, authority: "BH. Güssing" }
103
+ - { code: JE, land: Burgenland, authority: "BH. Jennersdorf" }
104
+ # II. KÄRNTEN
105
+ - { code: K, land: Kärnten, authority: "Landespolizeidirektion Kärnten für das Gebiet der Gemeinde Klagenfurt am Wörthersee", landeshauptstadt: true }
106
+ - { code: VI, land: Kärnten, authority: "Landespolizeidirektion Kärnten für das Gebiet der Gemeinde Villach" }
107
+ - { code: VL, land: Kärnten, authority: "BH. Villach" }
108
+ - { code: WO, land: Kärnten, authority: "BH. Wolfsberg" }
109
+ - { code: SP, land: Kärnten, authority: "BH. Spittal an der Drau" }
110
+ - { code: FE, land: Kärnten, authority: "BH. Feldkirchen" }
111
+ - { code: HE, land: Kärnten, authority: "BH. Hermagor" }
112
+ - { code: SV, land: Kärnten, authority: "BH. St. Veit an der Glan" }
113
+ - { code: KL, land: Kärnten, authority: "BH. Klagenfurt" }
114
+ - { code: VK, land: Kärnten, authority: "BH. Völkermarkt" }
115
+ # III. NIEDERÖSTERREICH
116
+ - { code: P, land: Niederösterreich, authority: "Landespolizeidirektion Niederösterreich für das Gebiet der Gemeinde St. Pölten", landeshauptstadt: true }
117
+ - { code: AM, land: Niederösterreich, authority: "BH. Amstetten" }
118
+ - { code: BN, land: Niederösterreich, authority: "BH. Baden" }
119
+ - { code: BL, land: Niederösterreich, authority: "BH. Bruck an der Leitha" }
120
+ - { code: GF, land: Niederösterreich, authority: "BH. Gänserndorf" }
121
+ - { code: GD, land: Niederösterreich, authority: "BH. Gmünd" }
122
+ - { code: HL, land: Niederösterreich, authority: "BH. Hollabrunn" }
123
+ - { code: HO, land: Niederösterreich, authority: "BH. Horn" }
124
+ - { code: KO, land: Niederösterreich, authority: "BH. Korneuburg" }
125
+ - { code: KR, land: Niederösterreich, authority: "BH. Krems an der Donau" }
126
+ - { code: LF, land: Niederösterreich, authority: "BH. Lilienfeld" }
127
+ - { code: ME, land: Niederösterreich, authority: "BH. Melk" }
128
+ - { code: MI, land: Niederösterreich, authority: "BH. Mistelbach" }
129
+ - { code: MD, land: Niederösterreich, authority: "BH. Mödling" }
130
+ - { code: NK, land: Niederösterreich, authority: "BH. Neunkirchen" }
131
+ - { code: PL, land: Niederösterreich, authority: "BH. St. Pölten" }
132
+ - { code: SB, land: Niederösterreich, authority: "BH. Scheibbs" }
133
+ - { code: TU, land: Niederösterreich, authority: "BH. Tulln" }
134
+ - { code: WT, land: Niederösterreich, authority: "BH. Waidhofen an der Thaya" }
135
+ - { code: WB, land: Niederösterreich, authority: "BH. Wiener Neustadt" }
136
+ - { code: ZT, land: Niederösterreich, authority: "BH. Zwettl" }
137
+ - { code: WN, land: Niederösterreich, authority: "Landespolizeidirektion Niederösterreich für das Gebiet der Gemeinde Wr. Neustadt" }
138
+ - { code: KS, land: Niederösterreich, authority: "Mag. Krems an der Donau" }
139
+ - { code: WY, land: Niederösterreich, authority: "Mag. Waidhofen an der Ybbs" }
140
+ - { code: SW, land: Niederösterreich, authority: "Landespolizeidirektion Niederösterreich für das Gebiet der Gemeinde Schwechat und die im Gebiet der Gemeinden Fischamend, Klein-Neusiedl und Schwadorf gelegenen Teile des Flughafens Wien-Schwechat" }
141
+ - { code: KG, land: Niederösterreich, authority: "BH Tulln für das Gebiet der Gemeinde Klosterneuburg (Außenstelle Klosterneuburg)" }
142
+ # IV. OBERÖSTERREICH
143
+ - { code: L, land: Oberösterreich, authority: "Landespolizeidirektion Oberösterreich für das Gebiet der Gemeinde Linz", landeshauptstadt: true }
144
+ - { code: BR, land: Oberösterreich, authority: "BH. Braunau am Inn" }
145
+ - { code: EF, land: Oberösterreich, authority: "BH. Eferding" }
146
+ - { code: FR, land: Oberösterreich, authority: "BH. Freistadt" }
147
+ - { code: GM, land: Oberösterreich, authority: "BH. Gmunden" }
148
+ - { code: GR, land: Oberösterreich, authority: "BH. Grieskirchen" }
149
+ - { code: KI, land: Oberösterreich, authority: "BH. Kirchdorf an der Krems" }
150
+ - { code: LL, land: Oberösterreich, authority: "BH. Linz-Land" }
151
+ - { code: PE, land: Oberösterreich, authority: "BH. Perg" }
152
+ - { code: RI, land: Oberösterreich, authority: "BH. Ried im Innkreis" }
153
+ - { code: RO, land: Oberösterreich, authority: "BH. Rohrbach im Mühlkreis" }
154
+ - { code: SD, land: Oberösterreich, authority: "BH. Schärding" }
155
+ - { code: SE, land: Oberösterreich, authority: "BH. Steyr-Land" }
156
+ - { code: UU, land: Oberösterreich, authority: "BH. Urfahr-Umgebung" }
157
+ - { code: VB, land: Oberösterreich, authority: "BH. Vöcklabruck" }
158
+ - { code: WL, land: Oberösterreich, authority: "BH. Wels" }
159
+ - { code: SR, land: Oberösterreich, authority: "Landespolizeidirektion Oberösterreich für das Gebiet der Gemeinde Steyr" }
160
+ - { code: WE, land: Oberösterreich, authority: "Landespolizeidirektion Oberösterreich für das Gebiet der Gemeinde Wels" }
161
+ # V. SALZBURG
162
+ - { code: S, land: Salzburg, authority: "Landespolizeidirektion Salzburg für das Gebiet der Gemeinde Salzburg", landeshauptstadt: true }
163
+ - { code: SL, land: Salzburg, authority: "BH. Salzburg-Umgebung" }
164
+ - { code: HA, land: Salzburg, authority: "BH. Hallein" }
165
+ - { code: JO, land: Salzburg, authority: "BH. St. Johann" }
166
+ - { code: ZE, land: Salzburg, authority: "BH. Zell am See" }
167
+ - { code: TA, land: Salzburg, authority: "BH. Tamsweg", note: "the consolidated annex prints 'BH- Tamsweg' — a typo in the instrument, reproduced here as a finding, not corrected in the source" }
168
+ # VI. STEIERMARK
169
+ - { code: G, land: Steiermark, authority: "Landespolizeidirektion Steiermark für das Gebiet der Gemeinde Graz", landeshauptstadt: true }
170
+ - { code: GU, land: Steiermark, authority: "BH. Graz-Umgebung" }
171
+ - { code: BM, land: Steiermark, authority: "BH. Bruck-Mürzzuschlag" }
172
+ - { code: DL, land: Steiermark, authority: "BH. Deutschlandsberg" }
173
+ - { code: HF, land: Steiermark, authority: "BH. Hartberg-Fürstenfeld" }
174
+ - { code: MT, land: Steiermark, authority: "BH. Murtal" }
175
+ - { code: LB, land: Steiermark, authority: "BH. Leibnitz" }
176
+ - { code: LN, land: Steiermark, authority: "BH. Leoben" }
177
+ - { code: LI, land: Steiermark, authority: "BH. Liezen" }
178
+ - { code: MU, land: Steiermark, authority: "BH. Murau" }
179
+ - { code: SO, land: Steiermark, authority: "BH. Südoststeiermark" }
180
+ - { code: VO, land: Steiermark, authority: "BH. Voitsberg" }
181
+ - { code: WZ, land: Steiermark, authority: "BH. Weiz" }
182
+ - { code: GB, land: Steiermark, authority: "Exp. Gröbming" }
183
+ - { code: LE, land: Steiermark, authority: "Landespolizeidirektion Steiermark für das Gebiet der Gemeinde Leoben" }
184
+ - { code: BA, land: Steiermark, authority: "BH Liezen für das Gebiet der Gemeinden Bad Aussee, Altaussee, Grundlsee und Bad Mitterndorf (Außenstelle Bad Aussee)" }
185
+ # VII. TIROL
186
+ - { code: I, land: Tirol, authority: "Landespolizeidirektion Tirol für das Gebiet der Gemeinde Innsbruck", landeshauptstadt: true }
187
+ - { code: IL, land: Tirol, authority: "BH. Innsbruck" }
188
+ - { code: IM, land: Tirol, authority: "BH. Imst" }
189
+ - { code: KB, land: Tirol, authority: "BH. Kitzbühel" }
190
+ - { code: KU, land: Tirol, authority: "BH. Kufstein" }
191
+ - { code: LA, land: Tirol, authority: "BH. Landeck" }
192
+ - { code: RE, land: Tirol, authority: "BH. Reutte" }
193
+ - { code: SZ, land: Tirol, authority: "BH. Schwaz" }
194
+ - { code: LZ, land: Tirol, authority: "BH. Lienz" }
195
+ # VIII. VORARLBERG
196
+ - { code: B, land: Vorarlberg, authority: "BH. Bregenz" }
197
+ - { code: FK, land: Vorarlberg, authority: "BH. Feldkirch" }
198
+ - { code: BZ, land: Vorarlberg, authority: "BH. Bludenz" }
199
+ - { code: DO, land: Vorarlberg, authority: "BH. Dornbirn" }
200
+ # IX. WIEN
201
+ - { code: W, land: Wien, authority: "Landespolizeidirektion Wien", landeshauptstadt: true }
202
+
203
+ landeshauptstadt_note: >-
204
+ `landeshauptstadt: true` marks the codes this pass reads as
205
+ "in den Landeshauptstädten ... zugewiesen" for the purposes of § 26 Abs. 6
206
+ Z 2 lit. a KDV (one Vormerkzeichen character MORE than elsewhere). It is
207
+ flagged as an INFERENCE, not a primary: the annex names those authorities
208
+ "Landespolizeidirektion X für das Gebiet der Gemeinde <Landeshauptstadt>",
209
+ which is why the nine are picked out, but the KDV never says that the
210
+ extra character attaches to those CODES. Vorarlberg is the case that proves
211
+ the inference is not safe: its Landeshauptstadt is Bregenz, whose plates
212
+ are issued by "BH. Bregenz" (code B) over a whole district, and the annex
213
+ gives Vorarlberg no Landespolizeidirektion row at all — so B is left
214
+ unmarked here and the question is a recorded gap.
215
+
216
+ # ---------------------------------------------------------------------------
217
+ # THE NON-GEOGRAPHIC PREFIXES — § 26 KDV itself, not Anlage 5d.
218
+ # ---------------------------------------------------------------------------
219
+ federal_letter:
220
+ source: "https://www.ris.bka.gv.at/NormDokument.wxe?Abfrage=Bundesnormen&Gesetzesnummer=10011385&Paragraf=26 # § 26 Abs. 2"
221
+ A: >-
222
+ Fahrzeuge, die zur Verwendung für den Bundespräsidenten, die Präsidenten des
223
+ Nationalrates, die Präsidenten des Bundesrates, die Mitglieder der
224
+ Bundesregierung, die Staatssekretäre, die Mitglieder der Volksanwaltschaft,
225
+ den Präsidenten oder Vizepräsidenten des Rechnungshofes, des
226
+ Verfassungsgerichtshofes und Verwaltungsgerichtshofes oder des Obersten
227
+ Gerichtshofes bestimmt sind
228
+
229
+ land_letters:
230
+ source: "https://www.ris.bka.gv.at/NormDokument.wxe?Abfrage=Bundesnormen&Gesetzesnummer=10011385&Paragraf=26 # § 26 Abs. 3, verbatim list"
231
+ scope: >-
232
+ Fahrzeuge, die zur Verwendung für die Präsidenten der Landtage sowie für die
233
+ Mitglieder der Landesregierungen sowie für die Mitglieder der
234
+ Landesvolksanwaltschaften bestimmt sind
235
+ codes:
236
+ B: Burgenland
237
+ K: Kärnten
238
+ N: Niederösterreich
239
+ O: Oberösterreich
240
+ S: Salzburg
241
+ ST: Steiermark
242
+ T: Tirol
243
+ V: Vorarlberg
244
+ W: Wien
245
+ note: >-
246
+ ST is the only THREE-position prefix base in the system and the reason the
247
+ § 26 Abs. 5 Graz exception exists (see diplomatic_consular below). N and O
248
+ appear ONLY here — no Anlage 5d district carries them.
249
+
250
+ sachliche_bereiche:
251
+ source: "https://www.ris.bka.gv.at/NormDokument.wxe?Abfrage=Bundesnormen&Gesetzesnummer=10011385&Paragraf=26 # § 26 Abs. 4 lit. a-i (lit. b repealed by BGBl. II Nr. 275/2007, lit. d by BGBl. II Nr. 376/2002)"
252
+ codes:
253
+ BP: "Fahrzeuge, die zur Verwendung im Bereich der Bundespolizei bestimmt sind"
254
+ FV: "Fahrzeuge, die zur Verwendung im Bereich der Finanzverwaltung bestimmt sind"
255
+ PT: "Fahrzeuge, die zur Verwendung im Bereich der Post bestimmt sind"
256
+ BD: "Omnibusse, die zur Verwendung im Kraftfahrlinienverkehr der Österreichischen Bundesbahnen und der Post- und Telegraphenverwaltung (Bundesbusdienst) bestimmt sind"
257
+ BH: "Heeresfahrzeuge"
258
+ JW: "Fahrzeuge, die zur Verwendung im Bereich der Justizwache bestimmt sind"
259
+ FW: "Fahrzeuge, die zur Verwendung für die Feuerwehr bestimmt sind"
260
+ note: >-
261
+ FW is the ONE prefix whose serial encodes geography: § 26 Abs. 6 Z 1 lit. b
262
+ requires a fire-brigade Vormerkzeichen to "mit zwei oder drei Ziffern
263
+ beginnen und es folgen ein oder zwei Buchstaben, die der Bezeichnung der
264
+ Behörde im Sinne der Anlage 5d entsprechen müssen, in deren Sprengel das
265
+ Fahrzeug zugelassen ist" — so the district code moves to the END of the
266
+ string and the `codes` table above decodes the SUFFIX, not the prefix.
267
+
268
+ diplomatic_consular:
269
+ source: "https://www.ris.bka.gv.at/NormDokument.wxe?Abfrage=Bundesnormen&Gesetzesnummer=10011385&Paragraf=26 # § 26 Abs. 5 lit. a (D) and lit. b (K), plus the Graz exception in the opening sentence"
270
+ mechanism: >-
271
+ For the vehicles listed in § 54 Abs. 3 and Abs. 3a lit. a und b KFG 1967
272
+ (the CD and CC entitlement classes), the sachlicher Bereich is "die
273
+ Buchstaben gemäß Abs. 3" — i.e. the LAND letter — "bei Fahrzeugen mit dem
274
+ Standort in Graz ist an Stelle der Buchstaben gemäß Abs. 3 die Bezeichnung
275
+ der Behörde gemäß Anlage 5d zu verwenden" (so G, not ST). That base letter
276
+ is then followed by D for the diplomatic corps and internationally
277
+ equivalent officials, or K for the consular corps.
278
+ diplomatic_prefixes: [BD, KD, ND, OD, SD, STD, TD, VD, WD, GD]
279
+ consular_prefixes: [BK, KK, NK, OK, SK, STK, TK, VK, WK, GK]
280
+ derivation_note: >-
281
+ The twenty prefixes above are DERIVED by applying the § 26 Abs. 5 rule to
282
+ the § 26 Abs. 3 list plus the Graz exception; the KDV nowhere prints them as
283
+ a table. They are recorded here because a parse API needs them, and marked
284
+ as derived, not quoted. WD is overwhelmingly the populated one (the
285
+ diplomatic corps and the Vienna-based international organisations), and
286
+ several of the others may never have been issued — an unverified claim,
287
+ left unverified.
@@ -0,0 +1,110 @@
1
+ # plates/_decode/ch-cantons.yml — the Swiss canton letters. DRAFT for verification.
2
+ #
3
+ # THE SOURCE FINDING, and it is the good kind: this table is NOT a Wikipedia
4
+ # scrape and not an enthusiast list. It is enacted law, printed as a two-column
5
+ # table inside the ordinance itself — Verkehrszulassungsverordnung (VZV),
6
+ # SR 741.51, Art. 84 Abs. 1: "Jeder Kanton wird mit zwei grossen Buchstaben wie
7
+ # folgt bezeichnet:" followed by exactly the 26 rows below, in exactly this
8
+ # order. Every row here is that table, verbatim, in the ordinance's own order
9
+ # (the constitutional order of precedence, NOT alphabetical), with the French
10
+ # and Italian canton names taken from the FR and IT language versions of the
11
+ # same consolidated article.
12
+ #
13
+ # This is the German-Anlage-2 case and not the German-district case, and the
14
+ # contrast is worth stating because it drives the schema choice: § 9 Abs. 3 FZV
15
+ # delegates the ~400 German district codes to Bundesanzeiger publications, so
16
+ # plates/de.yml can only cite the MECHANISM; VZV Art. 84 Abs. 1 enumerates all
17
+ # 26 Swiss codes inside the regulation, so this table is closed, primary and
18
+ # complete on its face. There is no delegation, no open tail and no "on
19
+ # application by the canton" clause.
20
+ #
21
+ # Referenced by: plates/ch.yml (ch-standard, ch-standard-trailer, ch-motorcycle,
22
+ # ch-lightmotor-yellow, ch-motorfahrrad, ch-workvehicle-blue,
23
+ # ch-exceptional-brown, ch-agricultural-green, ch-diplomatic-cd,
24
+ # ch-consular-cc, ch-diplomatic-at, ch-provisional, ch-provisional-z,
25
+ # ch-dealer-u, ch-dealer-pre1977, ch-rental-1977) via region_encoding.
26
+ # NOT referenced by ch-military-m: federal military plates carry the single
27
+ # letter M and no canton code at all (VZV Art. 84 Abs. 3).
28
+ jurisdiction: ch
29
+ topic: cantons
30
+ authority:
31
+ name: Verkehrszulassungsverordnung (VZV), SR 741.51, Art. 84 Abs. 1
32
+ url: "https://www.fedlex.admin.ch/eli/cc/1976/2423_2423_2423/de"
33
+ sources:
34
+ - "https://www.fedlex.admin.ch/eli/cc/1976/2423_2423_2423/de # VZV Art. 84 Abs. 1 — the 26-row table, German names and codes, in the ordinance's own order; and the Jura footnote 'Kanton eingefügt durch Ziff. I der V vom 15. Nov. 1978, in Kraft seit 1. Jan. 1979 (AS 1978 1805)'"
35
+ - "https://www.fedlex.admin.ch/eli/cc/1976/2423_2423_2423/fr # OAC art. 84 al. 1 — the same table with the FRENCH canton names"
36
+ - "https://www.fedlex.admin.ch/eli/cc/1976/2423_2423_2423/it # OAC art. 84 cpv. 1 — the same table with the ITALIAN canton names"
37
+ accessed: "2026-08-02"
38
+ period: { start: 1977 }
39
+ period_evidence: instrument-in-force
40
+ period_note: >-
41
+ 1977 is the VZV's entry into force (1 January 1977), not the date the two-letter
42
+ canton abbreviations came into use — they are the ordinary Swiss cantonal
43
+ abbreviations and long predate the ordinance. No primary for their origin was
44
+ secured in this pass, so none is asserted. The ONE row with a real, dated,
45
+ primary start is JU: the ordinance's own footnote records the Jura row as
46
+ "Kanton eingefügt durch Ziff. I der V vom 15. Nov. 1978, in Kraft seit
47
+ 1. Jan. 1979 (AS 1978 1805)" — the canton of Jura separated from Bern and the
48
+ code entered the table on 1 January 1979. A plate allocated before that date
49
+ cannot read JU, and the territory JU covers read BE.
50
+
51
+ # Decode facts a parse API must carry.
52
+ maximal_munch: false
53
+ maximal_munch_note: >-
54
+ Not applicable, and that is a genuine simplification over Spain: every Swiss
55
+ code is EXACTLY two capital letters (VZV Art. 84 Abs. 1: "mit zwei grossen
56
+ Buchstaben"), so the letter/number boundary is fixed at position 2 and there is
57
+ no longest-match ambiguity of the C / CA / CC / CE kind that es-provinces.yml
58
+ has to warn about.
59
+ decode_semantics: >-
60
+ The code identifies the canton of the KEEPER's Standort, not the vehicle's
61
+ location, origin or manufacture. VZV Art. 77 Abs. 1 defines Standort as "der
62
+ Ort, wo das Fahrzeug nach Gebrauch in der Regel für die Nacht abgestellt wird",
63
+ with the keeper's domicile deemed the Standort in the three cases of Abs. 2.
64
+ Because the plate binds to the keeper (VZV Art. 87 Abs. 1), the code moves when
65
+ the KEEPER moves: VZV Art. 120 returns the annulled certificate and plates to
66
+ the former canton when a vehicle is admitted in another, and VZV Art. 95 Abs. 2
67
+ makes the same rule explicit for mopeds. A decoded canton is therefore a claim
68
+ about a person, not about a vehicle — the sharpest practical consequence of
69
+ Switzerland's owner-binding, and one a vehicle-keyed consumer will get wrong by
70
+ default.
71
+ collisions:
72
+ - "AG (Aargau), AI (Appenzell Innerrhoden), AR (Appenzell Ausserrhoden) — no collision with the diplomatic sign AT, which occupies its own field ahead of the canton letters (VZV Art. 85 Abs. 4), but a naive left-anchored parser reading 'AT BE 12-34' will try AT as a canton code and must not."
73
+ - "The letter M is NOT a canton code: it is the federal military marker of VZV Art. 84 Abs. 3, and it is a SINGLE letter, which is what distinguishes it structurally."
74
+ - "The letter U (dealer marking, VZV Art. 82 Abs. 2 lit. c) and the letter Z (uncleared provisional vehicle, VVV Anhang 2 A Ziff. 3) are markings, not canton codes; the ordinance does not fix their position, so a parser must not assume they trail the number even though that is the observed form."
75
+ codes:
76
+ - { code: ZH, de: "Zürich", fr: "Zurich", it: "Zurigo", period: { start: 1977 } }
77
+ - { code: BE, de: "Bern", fr: "Berne", it: "Berna", period: { start: 1977 } }
78
+ - { code: LU, de: "Luzern", fr: "Lucerne", it: "Lucerna", period: { start: 1977 } }
79
+ - { code: UR, de: "Uri", fr: "Uri", it: "Uri", period: { start: 1977 } }
80
+ - { code: SZ, de: "Schwyz", fr: "Schwyz", it: "Svitto", period: { start: 1977 } }
81
+ - { code: OW, de: "Obwalden", fr: "Obwald", it: "Obvaldo", period: { start: 1977 } }
82
+ - { code: NW, de: "Nidwalden", fr: "Nidwald", it: "Nidvaldo", period: { start: 1977 } }
83
+ - { code: GL, de: "Glarus", fr: "Glaris", it: "Glarona", period: { start: 1977 } }
84
+ - { code: ZG, de: "Zug", fr: "Zoug", it: "Zugo", period: { start: 1977 } }
85
+ - { code: FR, de: "Freiburg", fr: "Fribourg", it: "Friburgo", period: { start: 1977 } }
86
+ - { code: SO, de: "Solothurn", fr: "Soleure", it: "Soletta", period: { start: 1977 } }
87
+ - { code: BS, de: "Basel-Stadt", fr: "Bâle-Ville", it: "Basilea Città", period: { start: 1977 } }
88
+ - { code: BL, de: "Basel-Landschaft", fr: "Bâle-Campagne", it: "Basilea Campagna", period: { start: 1977 } }
89
+ - { code: SH, de: "Schaffhausen", fr: "Schaffhouse", it: "Sciaffusa", period: { start: 1977 } }
90
+ - { code: AR, de: "Appenzell A. Rh", fr: "Appenzell, Rhodes extérieures", it: "Appenzello esterno", period: { start: 1977 } }
91
+ - { code: AI, de: "Appenzell I. Rh.", fr: "Appenzell, Rhodes intérieures", it: "Appenzello interno", period: { start: 1977 } }
92
+ - { code: SG, de: "St. Gallen", fr: "Saint-Gall", it: "San Gallo", period: { start: 1977 } }
93
+ - { code: GR, de: "Graubünden", fr: "Grisons", it: "Grigioni", period: { start: 1977 } }
94
+ - { code: AG, de: "Aargau", fr: "Argovie", it: "Argovia", period: { start: 1977 } }
95
+ - { code: TG, de: "Thurgau", fr: "Thurgovie", it: "Turgovia", period: { start: 1977 } }
96
+ - { code: TI, de: "Tessin", fr: "Tessin", it: "Ticino", period: { start: 1977 } }
97
+ - { code: VD, de: "Waadt", fr: "Vaud", it: "Vaud", period: { start: 1977 } }
98
+ - { code: VS, de: "Wallis", fr: "Valais", it: "Vallese", period: { start: 1977 } }
99
+ - { code: NE, de: "Neuenburg", fr: "Neuchâtel", it: "Neuchâtel", period: { start: 1977 } }
100
+ - { code: GE, de: "Genf", fr: "Genève", it: "Ginevra", period: { start: 1977 } }
101
+ - { code: JU, de: "Jura", fr: "Jura", it: "Giura", period: { start: 1979 }, period_evidence: enabling-amendment, note: "row inserted by Ziff. I der V vom 15. Nov. 1978, in force 1 Jan. 1979 (AS 1978 1805); before that the territory registered as BE" }
102
+
103
+ # Not a decode dimension, recorded so nobody looks for one: the Swiss plate
104
+ # carries NO date, NO issuing-office code below canton level and NO vehicle-type
105
+ # code inside the serial. Vehicle type is signalled by the plate's BACKGROUND
106
+ # COLOUR and SIZE (VZV Art. 82 Abs. 1, Art. 83 Abs. 3), both of which are
107
+ # properties of the physical object rather than of the string. Number ranges are
108
+ # not published: VZV Art. 84 Abs. 2 says only that each per-canton, per-colour,
109
+ # per-marking counter starts "in der Regel mit der Zahl 1".
110
+ no_further_decode: true