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
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 99ccb43a3ef0091206a4e6a7445c48333886f0a26cfd9a1764397e0c5ec46516
4
- data.tar.gz: b2a878137e3861323aaf59f12c0f5f44052b5ed064bc2f519b7247aaca916027
3
+ metadata.gz: c9ab45bf3b30e2ab2688becf7e36d4c67a1f40267324cec67b3df7955eb874db
4
+ data.tar.gz: 2fac8f7b9d499000730c0ecb3e4fa98d2555a06ce1c7ce8e5d82a1c730ef9573
5
5
  SHA512:
6
- metadata.gz: c7c2b0995cb72a450f91ab1c7d37e8c1d0ebb439358a09572e467d4786dd0edca211c904d9194ef42f3e6563ba1719b32041c645d0d25bd2f85c663607e58840
7
- data.tar.gz: 0a3c8914e5d5fc681ef7b83b2062746b382f0281c0ae75475120acb3227d8d2d7349e1c2f0c2769f62e6f8f6130733430f4d2b25db3ce27780989bf9326f4bba
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,327 @@ 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
+
284
+ ## [0.6.1] - 2026-08-01
285
+
286
+ ### Added
287
+
288
+ - `Model#former_ids` — the full canonical ids a record absorbed through the
289
+ dataset's append-only migration contract (e.g. `car/alfa-romeo/159sw`).
290
+ The field was already in the bundled snapshot; the reader now surfaces it,
291
+ so integrators can migrate stored ids without re-parsing the raw JSON.
292
+ Also included in `Model#to_h`.
293
+
294
+ ## [0.6.0] - 2026-07-27
295
+
296
+ License plates become a first-class citizen: the PRD-PLATES registration-mark
297
+ dataset ships in the gem, bundled and offline like everything else.
298
+
299
+ ### Added
300
+ - `Vehicles.plate(input, jurisdiction:)` — two-tier plate validation:
301
+ exact as-issued, then separator-forgiving ("1234XYZ" == "1234 XYZ" ==
302
+ "1234-xyz") with `Match#suggestion` returning the as-issued formatting.
303
+ Leniency forgives punctuation, never the alphabet — strict regexes encode
304
+ what each authority really issues. `Match#strict?` separates
305
+ authority-alphabet hits from recall-only catch-alls (export plates etc.).
306
+ - `Vehicles.plates` / `Vehicles.plates(:nl)` — the full series data:
307
+ 73 series across NL/ES/DE/US-FL (gate L0, NL corpus-proven), each with
308
+ pattern, period, class, categories, sourced design facts and citations.
309
+ - Bundled data under `data/plates/` (CC-BY, pinned upstream commit in
310
+ `data/plates/PROVENANCE.md`).
311
+
312
+ The hosted images API is **live** — this release points the wired-up provider
313
+ seam at the real thing.
314
+
315
+ ### Added
316
+ - `Model#image(color:, size:)` returns a rendered variant URL from the live
317
+ VehiclesDB images API (`:sm` 320×180, `:md` 640×360, `:lg` 1280×720, webp).
318
+ Mint a key at <https://vehiclesdb.com/settings/api-keys> and set
319
+ `config.api_key`; without a key it stays nil, as always.
320
+ - `Model#images(color:)` returns the full payload: every variant with
321
+ dimensions, the rendered palette for the model, served-vs-requested color
322
+ (un-rendered colors fall back honestly instead of 404ing), provenance.
323
+
324
+ ### Changed
325
+ - `config.api_base_url` default is now `https://vehiclesdb.com` (the live API
326
+ origin; endpoint paths carry `/v1`). It must be an ORIGIN — no path suffix.
327
+ - `Model#image` accepts `year:` but does not send it: `year`/`trim` are
328
+ reserved filters server-side (the API 422s on them by contract) until
329
+ year-accurate renders ship.
330
+
10
331
  ## [0.4.1] - 2026-07-26
11
332
 
12
333
  Data-only refresh to VehiclesDB dataset **2026.07.4** — the July correction