vehicles 0.6.1 → 0.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (186) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop_todo.yml +164 -0
  3. data/CHANGELOG.md +274 -0
  4. data/README.md +55 -10
  5. data/data/PROVENANCE-vehicles.md +12 -0
  6. data/data/plates/PROVENANCE.md +5 -4
  7. data/data/plates/_decode/at-districts.yml +287 -0
  8. data/data/plates/_decode/ch-cantons.yml +110 -0
  9. data/data/plates/_decode/cn-issuing-offices.yml +463 -0
  10. data/data/plates/_decode/cn-provinces.yml +614 -0
  11. data/data/plates/_decode/cz-kraj.yml +159 -0
  12. data/data/plates/_decode/dk-serial-blocks.yml +327 -0
  13. data/data/plates/_decode/fr-departements.yml +191 -0
  14. data/data/plates/_decode/gb-age-identifiers.yml +170 -0
  15. data/data/plates/_decode/gb-memory-tags.yml +202 -0
  16. data/data/plates/_decode/gr-prefectures.yml +1408 -0
  17. data/data/plates/_decode/hr-cities.yml +315 -0
  18. data/data/plates/_decode/ie-counties.yml +227 -0
  19. data/data/plates/_decode/in-state-codes.yml +169 -0
  20. data/data/plates/_decode/is-districts-1938.yml +116 -0
  21. data/data/plates/_decode/it-provinces.yml +348 -0
  22. data/data/plates/_decode/jp-office-marks.yml +960 -0
  23. data/data/plates/_decode/jp-use-marks.yml +205 -0
  24. data/data/plates/_decode/kr-authority-marks.yml +146 -0
  25. data/data/plates/_decode/kr-use-syllables.yml +194 -0
  26. data/data/plates/_decode/lu-series-order.yml +638 -0
  27. data/data/plates/_decode/mx-entity-series.yml +380 -0
  28. data/data/plates/_decode/my-index-marks.yml +271 -0
  29. data/data/plates/_decode/no-counties-1913.yml +148 -0
  30. data/data/plates/_decode/no-districts.yml +901 -0
  31. data/data/plates/_decode/pl-powiats.yml +567 -0
  32. data/data/plates/_decode/pt-trailer-services.yml +96 -0
  33. data/data/plates/_decode/ro-counties.yml +213 -0
  34. data/data/plates/_decode/se-diplomatic.yml +220 -0
  35. data/data/plates/_decode/si-regions.yml +123 -0
  36. data/data/plates/_decode/sk-districts.yml +314 -0
  37. data/data/plates/_decode/th-registration-elements.yml +208 -0
  38. data/data/plates/_decode/ua-regions.yml +393 -0
  39. data/data/plates/_decode/za-licence-marks.yml +215 -0
  40. data/data/plates/_meta/classes.yml +1 -0
  41. data/data/plates/_meta/separators.yml +102 -0
  42. data/data/plates/ad.yml +961 -0
  43. data/data/plates/ar.yml +762 -0
  44. data/data/plates/at.yml +1424 -0
  45. data/data/plates/au-act.yml +472 -0
  46. data/data/plates/au-nsw.yml +282 -0
  47. data/data/plates/au-nt.yml +431 -0
  48. data/data/plates/au-qld.yml +272 -0
  49. data/data/plates/au-sa.yml +248 -0
  50. data/data/plates/au-tas.yml +302 -0
  51. data/data/plates/au-vic.yml +215 -0
  52. data/data/plates/au-wa.yml +309 -0
  53. data/data/plates/be.yml +1840 -0
  54. data/data/plates/br.yml +1152 -0
  55. data/data/plates/ca-ab.yml +430 -0
  56. data/data/plates/ca-bc.yml +697 -0
  57. data/data/plates/ca-mb.yml +499 -0
  58. data/data/plates/ca-nb.yml +537 -0
  59. data/data/plates/ca-nl.yml +743 -0
  60. data/data/plates/ca-ns.yml +808 -0
  61. data/data/plates/ca-nt.yml +610 -0
  62. data/data/plates/ca-nu.yml +280 -0
  63. data/data/plates/ca-on.yml +881 -0
  64. data/data/plates/ca-pe.yml +592 -0
  65. data/data/plates/ca-qc.yml +1036 -0
  66. data/data/plates/ca-sk.yml +256 -0
  67. data/data/plates/ca-yt.yml +656 -0
  68. data/data/plates/ch.yml +1273 -0
  69. data/data/plates/cn.yml +1431 -0
  70. data/data/plates/cy.yml +688 -0
  71. data/data/plates/cz.yml +1179 -0
  72. data/data/plates/de.yml +553 -118
  73. data/data/plates/dk.yml +1647 -0
  74. data/data/plates/ee.yml +1275 -0
  75. data/data/plates/es.yml +228 -32
  76. data/data/plates/fi.yml +1271 -0
  77. data/data/plates/fr.yml +1265 -0
  78. data/data/plates/gb.yml +1188 -0
  79. data/data/plates/gr.yml +721 -0
  80. data/data/plates/hr.yml +860 -0
  81. data/data/plates/hu.yml +959 -0
  82. data/data/plates/ie.yml +775 -0
  83. data/data/plates/in.yml +1172 -0
  84. data/data/plates/is.yml +667 -0
  85. data/data/plates/it.yml +1317 -0
  86. data/data/plates/jp.yml +3650 -0
  87. data/data/plates/kr.yml +1375 -0
  88. data/data/plates/li.yml +1010 -0
  89. data/data/plates/lt.yml +879 -0
  90. data/data/plates/lu.yml +842 -0
  91. data/data/plates/lv.yml +924 -0
  92. data/data/plates/mc.yml +1024 -0
  93. data/data/plates/mt.yml +854 -0
  94. data/data/plates/mx.yml +1372 -0
  95. data/data/plates/my.yml +685 -0
  96. data/data/plates/nl.yml +44 -0
  97. data/data/plates/no.yml +1018 -0
  98. data/data/plates/nz.yml +1699 -0
  99. data/data/plates/pl.yml +1296 -0
  100. data/data/plates/pt.yml +847 -0
  101. data/data/plates/ro.yml +785 -0
  102. data/data/plates/se.yml +1091 -0
  103. data/data/plates/si.yml +1105 -0
  104. data/data/plates/sk.yml +1439 -0
  105. data/data/plates/sm.yml +1119 -0
  106. data/data/plates/th.yml +1150 -0
  107. data/data/plates/ua.yml +1672 -0
  108. data/data/plates/us-ak.yml +763 -0
  109. data/data/plates/us-al.yml +457 -0
  110. data/data/plates/us-ar.yml +427 -0
  111. data/data/plates/us-as.yml +454 -0
  112. data/data/plates/us-az.yml +430 -0
  113. data/data/plates/us-ca.yml +892 -0
  114. data/data/plates/us-co.yml +452 -0
  115. data/data/plates/us-ct.yml +427 -0
  116. data/data/plates/us-dc.yml +614 -0
  117. data/data/plates/us-de.yml +551 -0
  118. data/data/plates/us-fl.yml +319 -96
  119. data/data/plates/us-ga.yml +644 -0
  120. data/data/plates/us-gu.yml +658 -0
  121. data/data/plates/us-hi.yml +585 -0
  122. data/data/plates/us-ia.yml +438 -0
  123. data/data/plates/us-id.yml +531 -0
  124. data/data/plates/us-il.yml +516 -0
  125. data/data/plates/us-in.yml +324 -0
  126. data/data/plates/us-ks.yml +351 -0
  127. data/data/plates/us-ky.yml +770 -0
  128. data/data/plates/us-la.yml +568 -0
  129. data/data/plates/us-ma.yml +379 -0
  130. data/data/plates/us-md.yml +536 -0
  131. data/data/plates/us-me.yml +445 -0
  132. data/data/plates/us-mi.yml +408 -0
  133. data/data/plates/us-mn.yml +411 -0
  134. data/data/plates/us-mo.yml +356 -0
  135. data/data/plates/us-mp.yml +402 -0
  136. data/data/plates/us-ms.yml +432 -0
  137. data/data/plates/us-mt.yml +619 -0
  138. data/data/plates/us-nc.yml +602 -0
  139. data/data/plates/us-nd.yml +419 -0
  140. data/data/plates/us-ne.yml +483 -0
  141. data/data/plates/us-nh.yml +394 -0
  142. data/data/plates/us-nj.yml +547 -0
  143. data/data/plates/us-nm.yml +574 -0
  144. data/data/plates/us-nv.yml +655 -0
  145. data/data/plates/us-ny.yml +592 -0
  146. data/data/plates/us-oh.yml +404 -0
  147. data/data/plates/us-ok.yml +513 -0
  148. data/data/plates/us-or.yml +392 -0
  149. data/data/plates/us-pa.yml +506 -0
  150. data/data/plates/us-pr.yml +507 -0
  151. data/data/plates/us-ri.yml +445 -0
  152. data/data/plates/us-sc.yml +605 -0
  153. data/data/plates/us-sd.yml +428 -0
  154. data/data/plates/us-tn.yml +471 -0
  155. data/data/plates/us-tx.yml +535 -0
  156. data/data/plates/us-ut.yml +384 -0
  157. data/data/plates/us-va.yml +567 -0
  158. data/data/plates/us-vi.yml +489 -0
  159. data/data/plates/us-vt.yml +458 -0
  160. data/data/plates/us-wa.yml +484 -0
  161. data/data/plates/us-wi.yml +509 -0
  162. data/data/plates/us-wv.yml +446 -0
  163. data/data/plates/us-wy.yml +523 -0
  164. data/data/plates/va.yml +604 -0
  165. data/data/plates/za.yml +1141 -0
  166. data/data/vehicles.json +160653 -105090
  167. data/lib/vehicles/configuration.rb +17 -3
  168. data/lib/vehicles/dataset.rb +73 -18
  169. data/lib/vehicles/fuzzy.rb +115 -0
  170. data/lib/vehicles/http.rb +48 -0
  171. data/lib/vehicles/make.rb +36 -6
  172. data/lib/vehicles/mcp_server.rb +5 -5
  173. data/lib/vehicles/model.rb +21 -2
  174. data/lib/vehicles/plates/series.rb +172 -18
  175. data/lib/vehicles/plates/template_fill.rb +188 -0
  176. data/lib/vehicles/plates/templates.rb +162 -0
  177. data/lib/vehicles/plates.rb +12 -0
  178. data/lib/vehicles/providers/hosted_provider.rb +28 -55
  179. data/lib/vehicles/validators/vehicle_make_validator.rb +3 -1
  180. data/lib/vehicles/validators/vehicle_model_validator.rb +1 -1
  181. data/lib/vehicles/version.rb +1 -1
  182. data/lib/vehicles.rb +32 -16
  183. data/rakelib/data_refresh.rake +109 -0
  184. data/rakelib/plates_refresh.rake +29 -0
  185. data/sig/vehicles.rbs +18 -0
  186. metadata +164 -2
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: d31bdc07c2c1c97fe6e2f3f6de9bd94545f75cd69629a0ecc49171d369d7fee6
4
- data.tar.gz: afc63731709c038bd56171d50a30ddb8a06253f5fc51e86d2b3f7d36c0540c05
3
+ metadata.gz: c9ab45bf3b30e2ab2688becf7e36d4c67a1f40267324cec67b3df7955eb874db
4
+ data.tar.gz: 2fac8f7b9d499000730c0ecb3e4fa98d2555a06ce1c7ce8e5d82a1c730ef9573
5
5
  SHA512:
6
- metadata.gz: 2e0b37318a2131f1c35ecaaca88e72d4d967d9ad134c8777a6f0c2dda0d4105792c7eb2bd749ba0891a94dbac2761b60c947004376b478a3bb66f78e6f01bf2b
7
- data.tar.gz: 6a215001415080252d699ee3a202043c9685f6286a8763896db3371a81f9458ab01afcb58ca253e2f8a4cac652782564050246fc493911854c4c3756154a089d
6
+ metadata.gz: a2ac35c05cbd762cd5f818291c30792c1b8e69c18efe699f5b9408f9a35b23bd2646217f4333ca09ca7ef75c54c464eb2dedfdf4433cb7a9563712d69116c90c
7
+ data.tar.gz: bfcc408a806116de713664e4acf300edc515e9bf0a98075f4fa943520683e13b1b0700fa616635e2cf3cc2e73d08db8f97ba4e3dd606ed726ef0b5d653827076
data/.rubocop_todo.yml ADDED
@@ -0,0 +1,164 @@
1
+ # This configuration was generated by
2
+ # `rubocop --auto-gen-config --no-exclude-limit`
3
+ # on 2026-10-10 04:04:37 UTC using RuboCop version 1.91.0.
4
+ # The point is for the user to remove these configuration records
5
+ # one by one as the offenses are removed from the code base.
6
+ # Note that changes in the inspected code, or installation of new
7
+ # versions of RuboCop, may require this file to be generated again.
8
+
9
+ # Offense count: 5
10
+ # This cop supports safe autocorrection (--autocorrect).
11
+ # Configuration parameters: EnforcedStyle, IndentationWidth.
12
+ # SupportedStyles: with_first_argument, with_fixed_indentation
13
+ Layout/ArgumentAlignment:
14
+ Exclude:
15
+ - 'lib/vehicles/plates/series.rb'
16
+ - 'test/plates_test.rb'
17
+
18
+ # Offense count: 1
19
+ # This cop supports safe autocorrection (--autocorrect).
20
+ # Configuration parameters: EmptyLineBetweenMethodDefs, EmptyLineBetweenClassDefs, EmptyLineBetweenModuleDefs, DefLikeMacros, AllowAdjacentOneLineDefs, NumberOfEmptyLines.
21
+ Layout/EmptyLineBetweenDefs:
22
+ Exclude:
23
+ - 'test/plates_test.rb'
24
+
25
+ # Offense count: 1
26
+ # This cop supports safe autocorrection (--autocorrect).
27
+ # Configuration parameters: AllowForAlignment, AllowBeforeTrailingComments, ForceEqualSignAlignment.
28
+ Layout/ExtraSpacing:
29
+ Exclude:
30
+ - 'lib/vehicles/plates/series.rb'
31
+
32
+ # Offense count: 1
33
+ # This cop supports safe autocorrection (--autocorrect).
34
+ # Configuration parameters: EnforcedStyle, IndentationWidth.
35
+ # SupportedStyles: aligned, indented, indented_relative_to_receiver
36
+ Layout/MultilineMethodCallIndentation:
37
+ Exclude:
38
+ - 'lib/vehicles/plates/jurisdiction.rb'
39
+
40
+ # Offense count: 1
41
+ # This cop supports safe autocorrection (--autocorrect).
42
+ # Configuration parameters: EnforcedStyle, IndentationWidth.
43
+ # SupportedStyles: aligned, indented
44
+ Layout/MultilineOperationIndentation:
45
+ Exclude:
46
+ - 'lib/vehicles/plates/series.rb'
47
+
48
+ # Offense count: 1
49
+ # This cop supports safe autocorrection (--autocorrect).
50
+ # Configuration parameters: AllowForAlignment, EnforcedStyleForExponentOperator, EnforcedStyleForRationalLiterals.
51
+ # SupportedStylesForExponentOperator: space, no_space
52
+ # SupportedStylesForRationalLiterals: space, no_space
53
+ Layout/SpaceAroundOperators:
54
+ Exclude:
55
+ - 'lib/vehicles/plates/series.rb'
56
+
57
+ # Offense count: 58
58
+ # This cop supports safe autocorrection (--autocorrect).
59
+ # Configuration parameters: EnforcedStyle, EnforcedStyleForEmptyBrackets.
60
+ # SupportedStyles: space, no_space, compact
61
+ # SupportedStylesForEmptyBrackets: space, no_space
62
+ Layout/SpaceInsideArrayLiteralBrackets:
63
+ Exclude:
64
+ - 'lib/vehicles/plates.rb'
65
+ - 'lib/vehicles/plates/jurisdiction.rb'
66
+ - 'lib/vehicles/plates/series.rb'
67
+ - 'test/plates_test.rb'
68
+
69
+ # Offense count: 1
70
+ # This cop supports unsafe autocorrection (--autocorrect-all).
71
+ Lint/RedundantDirGlobSort:
72
+ Exclude:
73
+ - 'lib/vehicles/plates.rb'
74
+
75
+ # Offense count: 4
76
+ # Configuration parameters: AllowedMethods, AllowedPatterns, CountRepeatedAttributes, Max.
77
+ Metrics/AbcSize:
78
+ Exclude:
79
+ - 'lib/vehicles/plates/series.rb'
80
+
81
+ # Offense count: 1
82
+ # Configuration parameters: CountBlocks, CountModifierForms, Max.
83
+ Metrics/BlockNesting:
84
+ Exclude:
85
+ - 'lib/vehicles/plates/series.rb'
86
+
87
+ # Offense count: 1
88
+ # Configuration parameters: CountComments, Max, CountAsOne.
89
+ Metrics/ClassLength:
90
+ Exclude:
91
+ - 'lib/vehicles/mcp_server.rb'
92
+ - 'lib/vehicles/plates/series.rb'
93
+
94
+ # Offense count: 2
95
+ # Configuration parameters: CountComments, Max, CountAsOne, AllowedMethods, AllowedPatterns.
96
+ Metrics/MethodLength:
97
+ Exclude:
98
+ - 'lib/vehicles/plates/series.rb'
99
+
100
+ # Offense count: 1
101
+ # This cop supports safe autocorrection (--autocorrect).
102
+ Minitest/AssertPredicate:
103
+ Exclude:
104
+ - 'test/model_test.rb'
105
+
106
+ # Offense count: 11
107
+ # This cop supports safe autocorrection (--autocorrect).
108
+ Minitest/EmptyLineBeforeAssertionMethods:
109
+ Exclude:
110
+ - 'test/model_test.rb'
111
+ - 'test/plates_test.rb'
112
+
113
+ # Offense count: 13
114
+ # Configuration parameters: MinSize.
115
+ Performance/CollectionLiteralInLoop:
116
+ Exclude:
117
+ - 'lib/vehicles/plates/series.rb'
118
+ - 'test/plates_test.rb'
119
+
120
+ # Offense count: 1
121
+ # This cop supports unsafe autocorrection (--autocorrect-all).
122
+ # Configuration parameters: EnforcedStyle.
123
+ # SupportedStyles: always, always_true, never
124
+ Style/FrozenStringLiteralComment:
125
+ Exclude:
126
+ - '**/*.arb'
127
+ - 'rakelib/plates_refresh.rake'
128
+
129
+ # Offense count: 1
130
+ # This cop supports safe autocorrection (--autocorrect).
131
+ Style/NestedTernaryOperator:
132
+ Exclude:
133
+ - 'lib/vehicles/plates/series.rb'
134
+
135
+ # Offense count: 1
136
+ # This cop supports safe autocorrection (--autocorrect).
137
+ Style/PerlBackrefs:
138
+ Exclude:
139
+ - 'lib/vehicles/plates/series.rb'
140
+
141
+ # Offense count: 2
142
+ # This cop supports safe autocorrection (--autocorrect).
143
+ # Configuration parameters: EnforcedStyle, ConsistentQuotesInMultiline.
144
+ # SupportedStyles: single_quotes, double_quotes
145
+ Style/StringLiterals:
146
+ Exclude:
147
+ - 'test/plates_test.rb'
148
+
149
+ # Offense count: 3
150
+ # This cop supports safe autocorrection (--autocorrect).
151
+ # Configuration parameters: EnforcedStyle.
152
+ # SupportedStyles: single_quotes, double_quotes
153
+ Style/StringLiteralsInInterpolation:
154
+ Exclude:
155
+ - 'lib/vehicles/plates.rb'
156
+ - 'lib/vehicles/plates/series.rb'
157
+
158
+ # Offense count: 1
159
+ # This cop supports safe autocorrection (--autocorrect).
160
+ # Configuration parameters: WordRegex.
161
+ # SupportedStyles: percent, brackets
162
+ Style/WordArray:
163
+ EnforcedStyle: percent
164
+ MinSize: 3
data/CHANGELOG.md CHANGED
@@ -7,6 +7,280 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [0.8.0] - 2026-11-08
11
+
12
+ ### Added
13
+
14
+ - **Plate art without sending the plate.** `Vehicles::Plates.render(registration,
15
+ jurisdiction:, region: nil, label: nil)` draws a registration on its series'
16
+ real VehiclesDB plate as an SVG, and nothing personal leaves the machine: the
17
+ gem picks the series locally (the bundled matcher; the only standard-class
18
+ hit, else the only hit — ambiguous plates such as German `MAB1234` answer
19
+ nil instead of the wrong plate), derives the shape (`NNNN LLL`), fetches the
20
+ BLANK template `GET {api_base_url}/plates/:code/:series_id/template.svg?shape=&region=`
21
+ and fills it here. No API key is sent (the route is public). Returns nil on
22
+ any failure, never raises.
23
+ - `Vehicles::Plates.template(code:, series:, shape: nil, region: nil)`: the
24
+ blank template, or nil. Client-side fences refuse a shape that is not pure
25
+ N/L classes and separators, so a registration passed by mistake never
26
+ reaches the wire. Cached in-process by default; `config.plate_template_cache`
27
+ takes any `read` / `write(key, value, expires_in:)` store (e.g. `Rails.cache`)
28
+ and `config.plate_template_ttl` (default one day) the lifetime. Failures are
29
+ remembered for five minutes, so an unreachable server costs one timeout per
30
+ template per window, not one per call. Keys carry the template schema and
31
+ gem version.
32
+ - `Vehicles::Plates::TemplateFill`: the reference filler, moved verbatim from
33
+ vehiclesdb-web (`fill`, `metadata`, `groups`, and the shared fit formulas
34
+ `fit_size` / `recentered_baseline` the VehiclesDB renderer will call from
35
+ here), plus `shape_of` and a `label:` option for a localized accessible
36
+ name. Standard library only; the file loads on its own in a gem-less Ruby.
37
+ - Templates are inlined into host pages, so both the fetch and
38
+ `TemplateFill.fill` itself refuse a template carrying anything active or
39
+ external (`<script>`, event handlers including `<svg/onload=…>`,
40
+ `javascript:`, non-`data:` hrefs and `url()`s): `TemplateFill.inert?`.
41
+ - `region:` takes subdivision keys only (`75`, `2A`, `971`, `44-bzh`, `BY`,
42
+ `ZH`, `9`), so a registration passed there by mistake is refused locally
43
+ and never sent.
44
+
45
+ ### Changed
46
+
47
+ - The HTTP plumbing (timeout, no silent retry, nil on failure) moved from
48
+ `HostedProvider` into `Vehicles::HTTP`, shared by images and plate
49
+ templates. Behaviour of the images calls is unchanged.
50
+
51
+ ## [0.7.9] - 2026-11-08
52
+
53
+ ### Fixed
54
+
55
+ - **Hosted images now reach the API.** `HostedProvider` requested
56
+ `https://vehiclesdb.com/v1/vehicles/…/images`, but the VehiclesDB API lives
57
+ under **`/api/v1`**: `/v1/…` is the website's HTML 404, so `Model#image` /
58
+ `Model#images` returned nil for every key since 0.5.0. Requests now go to
59
+ `GET {api_base_url}/api/v1/vehicles/:kind/:make_slug/:model_slug/images`
60
+ (`HostedProvider::API_PREFIX`). `config.api_base_url` is still the origin
61
+ and still overridable (`"http://localhost:3030"` for a local server); a
62
+ trailing slash or a proxy path prefix on it is now kept as written
63
+ instead of being dropped by `URI.join`. Pinned by tests over a real local
64
+ socket: the exact request line, the Bearer header, and nil on a 404 HTML
65
+ page, a 401, a non-JSON 200 and a refused connection.
66
+ - **One hosted request is one request.** Net::HTTP re-sends an idempotent
67
+ GET once after a read timeout, so a slow images response was asked for
68
+ twice: double the latency `api_timeout` promises to cap, and two billed
69
+ calls when the second one is served (seen against a cold local server).
70
+ `max_retries` is now 0; pinned by a socket test that counts connections.
71
+ - **A name that exists as several kinds answers with the car.**
72
+ `Vehicles.model("Toyota", "Land Cruiser")` returned the **bus** (the first
73
+ record of four kinds). Without `kind:`, `Make#model` / `Vehicles.model` /
74
+ `Vehicles.find` now prefer the `:car` record and keep the first record
75
+ when there is no car. Over every model name and slug in the bundled data
76
+ this moves 51 names from bus to car — same make and model, never a
77
+ different one.
78
+
79
+ ### Added
80
+
81
+ - `kind:` on `Vehicles.model` and `Make#model` to pick the kind explicitly:
82
+ `Vehicles.model("Toyota", "Land Cruiser", kind: :bus)`.
83
+ - **Spacing-blind model names.** When no name matches exactly, `Make#model`
84
+ compares letters and digits only, so `"XCeed"` is Kia's `"X Ceed"`, `"A 5"`
85
+ is Audi `"A5"`, `"CX5"` is `"CX-5"`, `"XC 90"` is `"XC90"` — but only when
86
+ that lands on ONE name (never a guess between two), and never partially
87
+ (`"X Cee"` stays nil). The model validator answers the same way: spacing
88
+ is not a typo.
89
+ - **BMW's Spanish/Italian `"Serie N"`** reads as `"N Series"`
90
+ (`Vehicles.model("BMW", "Serie 3")` → 3 Series); a real `"Serie …"` name
91
+ (`"Serie X1"`) still wins.
92
+ - Measured on a Spanish carpooling app's 1,329 free-text vehicles (make/model
93
+ typed or picked in Spanish): 17 more make/model pairs (23 vehicles) now
94
+ resolve, and the 3 Land Cruisers resolve to the car. Internal double
95
+ spaces were already collapsed by `Vehicles.normalize`; now pinned by a test.
96
+
97
+ ### Changed
98
+
99
+ - `Model#years` / `Model#segment` no longer spend a request: the API serves
100
+ no endpoint for them yet (the old `/v1/models/:slug` never existed), so
101
+ the hosted provider answers nil without touching the network and the
102
+ chain falls through to the local data, exactly as before. They light up
103
+ when the API ships them. `HostedProvider.reset!` (its per-process cache)
104
+ is gone with it.
105
+ - README: the plates endpoints are `/api/v1/plates/…`.
106
+
107
+ ## [0.7.8] - 2026-10-10
108
+
109
+ ### Added
110
+
111
+ - **Typo-tolerant make lookup.** `Vehicles.make`, `Vehicles.models` and
112
+ every make-taking helper now fall back to the closest make by edit
113
+ distance when nothing matches exactly: `"marcedes"` → Mercedes-Benz,
114
+ `"volkswagon"` → Volkswagen, `"DeLorean"` → De Lorean. Bounded
115
+ (Levenshtein 1 for 4-5 characters, 2 from 6 up, never under 4), stdlib
116
+ only, and never a guess: when two different makes are equally close
117
+ (`"Acra"`: Acura / Acma) the answer is `nil`. Exact, alias and slug
118
+ matches always win; `Vehicles.find` runs its exact pass over every
119
+ make/model split before retrying a misspelled make, so no answer that
120
+ resolved in 0.7.7 changes. Opt out per call with `fuzzy: false`
121
+ (`make`, `models`, `model`, `model_options`, `find`). Guards: optimal
122
+ string alignment (an adjacent swap is one edit — `"telsa"` ties Tesla/
123
+ Temsa → nil, never Temsa); a candidate never has fewer words than the
124
+ query (`"Volvo V 60"` cannot become Volvo "60"); every word keeps its
125
+ first letter (`"citroen e"` is not Citroën DS); the configured
126
+ `other_label` is never fuzzy-matched. Verified: 0 changed / 0 lost
127
+ answers over 37,050 `find` and 65,871 web-resolver-style queries.
128
+ - `Model#country_ranks` / `Model#rank_in(country)` — the model's rank
129
+ among all models of its kind in that country (dataset key
130
+ `country_ranks`, from `catalog/` `popularity.by_country[cc].rank`).
131
+ - `Model#mass_decile` — per-kind registration-mass decile (1 = heaviest),
132
+ the dataset's `popularity.mass_decile` (2026.10.1+, pipeline#255 /
133
+ ruling R3); nil-safe on older snapshots (the bundled 2026.10.0 has none).
134
+ `global_decile` stays what it is — a presence average.
135
+ `top_models(by: :mass_decile)` ranks by it (records without it are
136
+ excluded, so it is `[]` on 2026.10.0 data); `by:` defaults to
137
+ `:global_decile`; anything else raises ArgumentError. With `country:`
138
+ the country's rank still sorts first (the decile breaks ties).
139
+ `Model#to_h` gains `:mass_decile`.
140
+ - `rake "data:refresh[<tag>]"` — refreshes `data/vehicles.json` from a
141
+ dataset release tag and writes `data/PROVENANCE-vehicles.md`; fills
142
+ `country_ranks` from the same tag's catalog until the projection ships
143
+ the key natively (then it asserts equality instead).
144
+
145
+ ### Changed
146
+
147
+ - **`top_models(country:)` orders by that country's own rank.** Until now a
148
+ country-filtered list kept the GLOBAL order (`global_decile`, a
149
+ presence-weighted average across countries), so the UK top-10 read in
150
+ everybody else's order. Models available there without a rank there
151
+ follow in the old order; data without `country_ranks` degrades to the
152
+ 0.7.7 order. Ranks are per kind — pass `kind:` for a clean ranking.
153
+ - Bundled dataset refreshed 2026.07.4 → **2026.10.0** (14,997 models, 15
154
+ countries; `dist/vehicles.json` at tag v2026.10.0 + `country_ranks`).
155
+ - The make and model validators stay EXACT (`fuzzy: false`): a typo never
156
+ validates as a stored make.
157
+
158
+ - Fuzzy matching is for human-typed names only: Symbols, input containing
159
+ `/`, `_`, `+` or `\` (ids, paths), hyphen-edged strings and single-case
160
+ hyphenated slugs (`marcedes-benz`, `MARCEDES-BENZ`) resolve exactly or
161
+ not at all; the MCP server's tools are exact-only.
162
+ - CI: the 111 rubocop offenses inherited from main are frozen (110 in a
163
+ generated `.rubocop_todo.yml`, plus `plates/series.rb` in `.rubocop.yml`'s
164
+ own ClassLength Exclude, which overrides the todo) — no code changes — so
165
+ the Lint job is green again; new code is fully linted.
166
+
167
+ ### Upgrade notes
168
+
169
+ - Apps that route or 404 on `Vehicles.make(param)`: slug-shaped params
170
+ (`marcedes-benz`, anything with `/`, `_`, `+`, a Symbol) never fuzz, but
171
+ a single-word param still does (`/makes/marcedes` → param `"marcedes"` →
172
+ Mercedes-Benz). If a misspelled URL must 404 (or 301 to the canonical
173
+ slug), call `Vehicles.make(param, fuzzy: false)` there.
174
+
175
+ ## [0.7.7] - 2026-08-03
176
+
177
+ ### Changed
178
+
179
+ - Plates dataset refreshed — the overnight swarm passes' settlements
180
+ land: the Cypriot centre date mark, Brazil's square-dot printed form
181
+ and photographed moto split, San Marino's structured legend/sigla
182
+ fields, Ukraine's statutory line counts, California's pixel-sampled
183
+ colours and corner-sticker guides, the Texas exempt restatement,
184
+ Korea's corporate characters block, Thailand's seven-photo
185
+ cross-check, and China's two-row trailer + NEV dedicated-mark flips
186
+ (#288-#290).
187
+
188
+ ## [0.7.5] - 2026-08-03
189
+
190
+ ### Added
191
+
192
+ - `Plates.decode_table(name)` — the _decode lookups (district/office/
193
+ use-mark tables) exposed to consumers, enabling serial-driven
194
+ subregion resolution (renderers picking the right Land arms from a
195
+ district prefix).
196
+
197
+ ## [0.7.4] - 2026-08-03
198
+
199
+ ### Added
200
+
201
+ - South Korea (21 series + hangul decode tables) — corpus: 124
202
+ jurisdictions / 1,381 series.
203
+
204
+ ## [0.7.3] - 2026-08-03
205
+
206
+ ### Fixed
207
+
208
+ - Pattern-language literal escapes: ca-nb-truck-2003 introduced "\L"
209
+ (a literal L, vs the any-letter placeholder). New Series#display_pattern
210
+ resolves escapes for display and slot walks; the round-trip test
211
+ substitutes placeholders escape-aware. Escape convention flagged to the
212
+ data maintainers for a ruling.
213
+
214
+ ## [0.7.2] - 2026-08-03
215
+
216
+ ### Added
217
+
218
+ - The L3 wave: 28 jurisdiction files across five continents (jp cn in
219
+ th my br mx ar nz za ua + Australia's 8 states + Canada's 13
220
+ provinces/territories), 415 series and 10 more decode tables incl.
221
+ Japan's 138-mark statutory office table. Corpus now: 123
222
+ jurisdictions / 1,360 series / 34 decode tables.
223
+
224
+ ## [0.7.1] - 2026-08-02
225
+
226
+ ### Added
227
+
228
+ - Plates corpus refreshed to 91 jurisdictions (the EU/EEA wave-2 landing:
229
+ ie dk no is pt lu pl cz sk hu ro si hr ee lv lt gr cy mt + microstates)
230
+ with 22 decode tables.
231
+
232
+ ## [0.7.0] - 2026-08-02
233
+
234
+ ### Added
235
+
236
+ - The FULL plates corpus ships in the gem: 67 jurisdictions / 606 series
237
+ (the 8-country L1 EU wave, all 50 US states + DC + 5 territories, and
238
+ the redated pilots) + 10 decode tables — up from the 4 pilot files.
239
+ The art tier stays in the data repo; the gem bundles format data.
240
+
241
+ ## [0.6.4] - 2026-08-02
242
+
243
+ ### Changed
244
+
245
+ - Bundled plates data refreshed from the redate pass: es-provincial is now
246
+ 1971-2000 pinned to Decreto 2046/1971 at both ends (was a 1999-2000
247
+ instrument window), plus the 1900-1971 Spanish numeric era, the German
248
+ 1956-2000 DIN era, 11 German and 10 Florida period corrections — all
249
+ cited to gazette primaries.
250
+ - `approximate_start?` covers the two new honesty vocabularies
251
+ (instrument-in-force-upper-bound, secondary-locator-unverified), so
252
+ upper-bounded and uncited-secondary starts render with the "~".
253
+
254
+ ## [0.6.3] - 2026-08-02
255
+
256
+ ### Changed
257
+
258
+ - Plate period labels read naturally: open series say "2000 → today", and
259
+ instrument-dated starts mark approximation as "~1999" instead of the
260
+ cryptic "≤1999". New `Series#approximate_start?` so displays can explain
261
+ the tilde ("documented from this year; the format may be older").
262
+
263
+ ## [0.6.2] - 2026-08-02
264
+
265
+ ### Fixed
266
+
267
+ - `Plates::Series#format_serial` reconstructed variable-length serials by
268
+ walking the fixed display pattern, misplacing separators ("M1234LA" came
269
+ back as "M1-234L-A" instead of "M-1234-LA"). The pattern walk now applies
270
+ only when the serial exactly fills the pattern; length-mismatched serials
271
+ reconstruct from the regex segmented at its separators (new
272
+ `Series.segment_separators`, riding the dataset separator contract), and
273
+ a serial neither path can place comes back untouched rather than
274
+ mis-punctuated.
275
+
276
+ ### Changed
277
+
278
+ - `Plates::Series#period_label` no longer presents instrument-dated starts
279
+ as the series era: rows whose `period_evidence` is `instrument-in-force`
280
+ or `instrument-window` label as "≤1999–2000" ("began by", not "began
281
+ in"). New `Series#period_evidence`, `#issued_regexp`,
282
+ `#issued_separators` readers for consumers mirroring the reconstruction.
283
+
10
284
  ## [0.6.1] - 2026-08-01
11
285
 
12
286
  ### Added
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
 
@@ -451,6 +470,31 @@ Vehicles.plates(:es).series # => 18 series: formats, periods, sourced designs
451
470
 
452
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.
453
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
+
454
498
  ## 🔓 More with VehiclesDB
455
499
 
456
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:
@@ -550,6 +594,7 @@ Vehicles.catalog(kind: :car) # => { "Audi" => ["A3", ...], ... }
550
594
  Vehicles.make("Audi") # => Vehicles::Make | nil
551
595
  Vehicles.find("audi a3") # => Vehicles::Model | nil (one free-text string)
552
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)
553
598
  Vehicles.search("a3") # => [Vehicles::Model]
554
599
 
555
600
  # Popularity & availability (open data, measured from official registers)
@@ -584,7 +629,7 @@ Vehicles.reload! # drop the in-memory dataset (reload fro
584
629
 
585
630
  ## Where the data comes from
586
631
 
587
- 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.
588
633
 
589
634
  Every record is shaped like this:
590
635
 
@@ -602,7 +647,7 @@ Every record is shaped like this:
602
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.
603
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.
604
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).
605
- - **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.
606
651
  - **The gem code:** MIT.
607
652
 
608
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.
@@ -618,7 +663,7 @@ No magic, just good defaults:
618
663
 
619
664
  ## Roadmap
620
665
 
621
- 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 +
622
667
  availability, plus the MCP server. On the way:
623
668
 
624
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).
@@ -1,9 +1,10 @@
1
- # Bundled plates dataset (PRD-PLATES L0)
1
+ # Bundled plates dataset (PRD-PLATES, full corpus)
2
2
 
3
- Copy of `plates/` from the public data repo, refreshed on gem releases:
3
+ Copy of `plates/` from the public data repo (EXCLUDING `_art/`),
4
+ refreshed on gem releases:
4
5
 
5
6
  - **Source**: https://github.com/vehiclesdb/vehiclesdb `plates/`
6
- - **Commit**: `69c8dab39424cf50ff8d1a526216551c8a9cb12f`
7
+ - **Commit**: `c7dd65c6f4144d39c62706b7be10231a12bc27e1`
7
8
  - **License**: CC-BY 4.0 (see the data repo's LICENSE + ATTRIBUTION.md)
8
9
 
9
- Refresh: copy the source tree over this directory and update the commit line.
10
+ Refresh: `rake plates:refresh` (this file is generated by it).