@meterapp/vehicle-db 2.5.0 → 2.7.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.
- package/README.md +131 -4
- package/dist/data.d.ts +14 -0
- package/dist/data.js +1 -1
- package/dist/index.d.ts +75 -2
- package/dist/index.js +138 -3
- package/dist/search.d.ts +47 -0
- package/dist/search.js +482 -0
- package/package.json +26 -2
package/dist/search.js
ADDED
|
@@ -0,0 +1,482 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.searchVehicles = searchVehicles;
|
|
7
|
+
const data_1 = __importDefault(require("./data"));
|
|
8
|
+
const MATCH_RANK = {
|
|
9
|
+
exact: 0,
|
|
10
|
+
prefix: 1,
|
|
11
|
+
token: 2,
|
|
12
|
+
contains: 3,
|
|
13
|
+
fuzzy: 4,
|
|
14
|
+
};
|
|
15
|
+
// These are search-only colloquialisms. They do not rewrite catalog identities.
|
|
16
|
+
const MAKE_SEARCH_ALIASES = new Map([
|
|
17
|
+
["alfa romeo", ["alfa"]],
|
|
18
|
+
["bmw", ["bimmer", "beemer"]],
|
|
19
|
+
["chevrolet", ["chevy"]],
|
|
20
|
+
["general motors", ["gm"]],
|
|
21
|
+
["harley davidson", ["harley"]],
|
|
22
|
+
["mercedes benz", ["mercedes", "benz", "merc"]],
|
|
23
|
+
["volkswagen", ["vw"]],
|
|
24
|
+
]);
|
|
25
|
+
const sourceMaskById = new Map(data_1.default.sources.map((source, index) => [source.source_id, 2 ** index]));
|
|
26
|
+
const vehicleTypeNameById = new Map(data_1.default.vehicleTypes.map((type) => [type.vehicle_type_id, type.vehicle_type_name]));
|
|
27
|
+
let _searchIndex = null;
|
|
28
|
+
/**
|
|
29
|
+
* Finds canonical make and model candidates for a driver-entered query.
|
|
30
|
+
*
|
|
31
|
+
* Matching is deterministic and popularity-neutral: exact, prefix, token,
|
|
32
|
+
* substring, then conservative typo matching. Applications can request a
|
|
33
|
+
* larger candidate set and rerank within the returned `matchKind` using local
|
|
34
|
+
* recents or regional telemetry.
|
|
35
|
+
*/
|
|
36
|
+
function searchVehicles(query, options = {}) {
|
|
37
|
+
if (options.year != null && !Number.isInteger(options.year))
|
|
38
|
+
return [];
|
|
39
|
+
const parsedQueries = parseQueries(query, options.year);
|
|
40
|
+
if (parsedQueries.length === 0)
|
|
41
|
+
return [];
|
|
42
|
+
const sourceMask = options.sourceId == null ? undefined : sourceMaskById.get(options.sourceId);
|
|
43
|
+
if (options.sourceId != null && sourceMask == null)
|
|
44
|
+
return [];
|
|
45
|
+
const limit = normalizeLimit(options.limit);
|
|
46
|
+
if (limit === 0)
|
|
47
|
+
return [];
|
|
48
|
+
const index = getSearchIndex();
|
|
49
|
+
const rankedByKey = collectQueryInterpretations(index, parsedQueries, options, sourceMask, false);
|
|
50
|
+
preferUnmodifiedInterpretation(rankedByKey);
|
|
51
|
+
// Typo matching is a fallback, not filler after a good lexical match. This
|
|
52
|
+
// keeps an exact vehicle from being followed by unrelated near-spellings.
|
|
53
|
+
if (options.fuzzy !== false && rankedByKey.size === 0) {
|
|
54
|
+
for (const [key, result] of collectQueryInterpretations(index, parsedQueries, options, sourceMask, true)) {
|
|
55
|
+
rankedByKey.set(key, result);
|
|
56
|
+
}
|
|
57
|
+
preferUnmodifiedInterpretation(rankedByKey);
|
|
58
|
+
}
|
|
59
|
+
const ranked = [...rankedByKey.values()];
|
|
60
|
+
ranked.sort(compareRankedResults);
|
|
61
|
+
return ranked.slice(0, limit).map(({ result }) => result);
|
|
62
|
+
}
|
|
63
|
+
function preferUnmodifiedInterpretation(results) {
|
|
64
|
+
if (![...results.values()].some((result) => result.interpretationRank === 0))
|
|
65
|
+
return;
|
|
66
|
+
for (const [key, result] of results) {
|
|
67
|
+
if (result.interpretationRank > 0)
|
|
68
|
+
results.delete(key);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
function collectQueryInterpretations(index, parsedQueries, options, sourceMask, fuzzyOnly) {
|
|
72
|
+
const rankedByKey = new Map();
|
|
73
|
+
for (const parsed of parsedQueries) {
|
|
74
|
+
const interpretationResults = [];
|
|
75
|
+
collectMatches(index, parsed.text, parsed.year, parsed.interpretationRank, options, sourceMask, fuzzyOnly, interpretationResults, new Set());
|
|
76
|
+
for (const result of interpretationResults) {
|
|
77
|
+
const existing = rankedByKey.get(result.key);
|
|
78
|
+
if (!existing || compareRankedResults(result, existing) < 0) {
|
|
79
|
+
rankedByKey.set(result.key, result);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
return rankedByKey;
|
|
84
|
+
}
|
|
85
|
+
function collectMatches(index, query, year, interpretationRank, options, sourceMask, fuzzyOnly, results, includedKeys) {
|
|
86
|
+
for (const make of index.makes) {
|
|
87
|
+
const key = `make\u0000${make.makeId}`;
|
|
88
|
+
if (includedKeys.has(key))
|
|
89
|
+
continue;
|
|
90
|
+
const match = matchForms(query, make.forms, fuzzyOnly);
|
|
91
|
+
if (!match || (fuzzyOnly && match.kind !== "fuzzy"))
|
|
92
|
+
continue;
|
|
93
|
+
const availability = selectMakeAvailability(make.rows, year, options.vehicleTypeId, sourceMask);
|
|
94
|
+
if (availability.years.length === 0)
|
|
95
|
+
continue;
|
|
96
|
+
includedKeys.add(key);
|
|
97
|
+
results.push({
|
|
98
|
+
key,
|
|
99
|
+
rank: match.rank,
|
|
100
|
+
interpretationRank,
|
|
101
|
+
closeness: match.closeness,
|
|
102
|
+
result: {
|
|
103
|
+
kind: "make",
|
|
104
|
+
makeId: make.makeId,
|
|
105
|
+
makeName: make.makeName,
|
|
106
|
+
years: availability.years,
|
|
107
|
+
sourceIds: getSourceIds(availability.sourceMask),
|
|
108
|
+
matchKind: match.kind,
|
|
109
|
+
},
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
for (const model of index.models) {
|
|
113
|
+
if (options.vehicleTypeId != null && model.vehicleTypeId !== options.vehicleTypeId)
|
|
114
|
+
continue;
|
|
115
|
+
const key = `model\u0000${model.makeId}\u0000${model.modelForms[0].compact}\u0000${model.vehicleTypeId}`;
|
|
116
|
+
if (includedKeys.has(key))
|
|
117
|
+
continue;
|
|
118
|
+
const modelMatch = matchForms(query, model.modelForms, fuzzyOnly);
|
|
119
|
+
const makeMatch = matchForms(query, model.makeForms, fuzzyOnly);
|
|
120
|
+
const match = modelMatch ?? (makeMatch ? undefined : matchForms(query, model.combinedForms, fuzzyOnly));
|
|
121
|
+
if (!match || (fuzzyOnly && match.kind !== "fuzzy"))
|
|
122
|
+
continue;
|
|
123
|
+
const variants = selectModelVariants(model.rows, year, sourceMask);
|
|
124
|
+
if (variants.length === 0)
|
|
125
|
+
continue;
|
|
126
|
+
const representative = variants.reduce((best, variant) => compareRepresentativeVariants(variant, best) < 0 ? variant : best);
|
|
127
|
+
const combinedSourceMask = variants.reduce((mask, variant) => mask | variant.sourceMask, 0);
|
|
128
|
+
includedKeys.add(key);
|
|
129
|
+
results.push({
|
|
130
|
+
key,
|
|
131
|
+
rank: match.rank,
|
|
132
|
+
interpretationRank,
|
|
133
|
+
closeness: match.closeness,
|
|
134
|
+
result: {
|
|
135
|
+
kind: "model",
|
|
136
|
+
makeId: model.makeId,
|
|
137
|
+
makeName: model.makeName,
|
|
138
|
+
modelName: representative.modelName,
|
|
139
|
+
vehicleTypeId: model.vehicleTypeId,
|
|
140
|
+
vehicleTypeName: model.vehicleTypeName,
|
|
141
|
+
years: variants.map((variant) => variant.year),
|
|
142
|
+
sourceIds: getSourceIds(combinedSourceMask),
|
|
143
|
+
matchKind: match.kind,
|
|
144
|
+
variants: variants.map(({ year: variantYear, modelId, sourceIds }) => ({
|
|
145
|
+
year: variantYear,
|
|
146
|
+
modelId,
|
|
147
|
+
sourceIds,
|
|
148
|
+
})),
|
|
149
|
+
},
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
function getSearchIndex() {
|
|
154
|
+
if (_searchIndex)
|
|
155
|
+
return _searchIndex;
|
|
156
|
+
const makesById = new Map();
|
|
157
|
+
const makeRowsById = new Map();
|
|
158
|
+
for (const make of data_1.default.makes) {
|
|
159
|
+
const baseForm = normalizeSearchText(make.make_name);
|
|
160
|
+
const aliases = MAKE_SEARCH_ALIASES.get(baseForm.spaced) ?? [];
|
|
161
|
+
makesById.set(make.make_id, {
|
|
162
|
+
makeId: make.make_id,
|
|
163
|
+
makeName: make.make_name,
|
|
164
|
+
forms: uniqueForms([baseForm, ...aliases.map(normalizeSearchText)]),
|
|
165
|
+
rows: [],
|
|
166
|
+
});
|
|
167
|
+
makeRowsById.set(make.make_id, new Map());
|
|
168
|
+
}
|
|
169
|
+
const modelsByKey = new Map();
|
|
170
|
+
for (const row of data_1.default.models) {
|
|
171
|
+
const [year, makeId, modelId, modelNameIndex, vehicleTypeId, sourceMask] = row;
|
|
172
|
+
const make = makesById.get(makeId);
|
|
173
|
+
const modelName = data_1.default.modelNames[modelNameIndex];
|
|
174
|
+
const vehicleTypeName = vehicleTypeNameById.get(vehicleTypeId);
|
|
175
|
+
if (!make || modelName == null || vehicleTypeName == null)
|
|
176
|
+
continue;
|
|
177
|
+
const makeRows = makeRowsById.get(makeId);
|
|
178
|
+
const makeRowKey = `${year}\u0000${vehicleTypeId}`;
|
|
179
|
+
const existingMakeRow = makeRows.get(makeRowKey);
|
|
180
|
+
if (existingMakeRow)
|
|
181
|
+
existingMakeRow.sourceMask |= sourceMask;
|
|
182
|
+
else
|
|
183
|
+
makeRows.set(makeRowKey, { year, vehicleTypeId, sourceMask });
|
|
184
|
+
const modelForm = normalizeSearchText(modelName);
|
|
185
|
+
const key = `${makeId}\u0000${modelForm.compact}\u0000${vehicleTypeId}`;
|
|
186
|
+
let model = modelsByKey.get(key);
|
|
187
|
+
if (!model) {
|
|
188
|
+
model = {
|
|
189
|
+
makeId,
|
|
190
|
+
makeName: make.makeName,
|
|
191
|
+
vehicleTypeId,
|
|
192
|
+
vehicleTypeName,
|
|
193
|
+
makeForms: make.forms,
|
|
194
|
+
modelForms: [],
|
|
195
|
+
combinedForms: [],
|
|
196
|
+
modelFormMap: new Map(),
|
|
197
|
+
rows: [],
|
|
198
|
+
};
|
|
199
|
+
modelsByKey.set(key, model);
|
|
200
|
+
}
|
|
201
|
+
model.modelFormMap.set(`${modelForm.spaced}\u0000${modelForm.compact}`, modelForm);
|
|
202
|
+
model.rows.push(row);
|
|
203
|
+
}
|
|
204
|
+
for (const make of makesById.values()) {
|
|
205
|
+
make.rows = [...makeRowsById.get(make.makeId).values()];
|
|
206
|
+
}
|
|
207
|
+
const models = [...modelsByKey.values()].map(({ modelFormMap, ...model }) => {
|
|
208
|
+
model.modelForms = [...modelFormMap.values()];
|
|
209
|
+
model.combinedForms = uniqueForms(model.makeForms.flatMap((makeForm) => model.modelForms.map((modelForm) => normalizeSearchText(`${makeForm.spaced} ${modelForm.spaced}`))));
|
|
210
|
+
return model;
|
|
211
|
+
});
|
|
212
|
+
_searchIndex = {
|
|
213
|
+
makes: [...makesById.values()],
|
|
214
|
+
models,
|
|
215
|
+
};
|
|
216
|
+
return _searchIndex;
|
|
217
|
+
}
|
|
218
|
+
function normalizeSearchText(value) {
|
|
219
|
+
const spaced = value
|
|
220
|
+
.normalize("NFKD")
|
|
221
|
+
.replace(/[\u0300-\u036f]/g, "")
|
|
222
|
+
.toLocaleLowerCase("en-US")
|
|
223
|
+
.replace(/&/g, " and ")
|
|
224
|
+
.replace(/[\u2019']/g, "")
|
|
225
|
+
.replace(/[^a-z0-9]+/g, " ")
|
|
226
|
+
.trim()
|
|
227
|
+
.replace(/\s+/g, " ");
|
|
228
|
+
return {
|
|
229
|
+
spaced,
|
|
230
|
+
compact: spaced.replace(/\s/g, ""),
|
|
231
|
+
tokens: spaced.length === 0 ? [] : spaced.split(" "),
|
|
232
|
+
};
|
|
233
|
+
}
|
|
234
|
+
function uniqueForms(forms) {
|
|
235
|
+
const seen = new Set();
|
|
236
|
+
return forms.filter((form) => {
|
|
237
|
+
const key = `${form.spaced}\u0000${form.compact}`;
|
|
238
|
+
if (seen.has(key))
|
|
239
|
+
return false;
|
|
240
|
+
seen.add(key);
|
|
241
|
+
return true;
|
|
242
|
+
});
|
|
243
|
+
}
|
|
244
|
+
function parseQueries(query, optionYear) {
|
|
245
|
+
const fullText = normalizeSearchText(query);
|
|
246
|
+
if (fullText.spaced.length === 0)
|
|
247
|
+
return [];
|
|
248
|
+
const detectedYears = [...query.matchAll(/\b(?:19|20)\d{2}\b/g)].map((match) => Number(match[0]));
|
|
249
|
+
const uniqueYears = [...new Set(detectedYears)];
|
|
250
|
+
const onlyYearTokens = fullText.tokens.length > 0 && fullText.tokens.every((token) => {
|
|
251
|
+
const value = Number(token);
|
|
252
|
+
return Number.isInteger(value) && uniqueYears.includes(value);
|
|
253
|
+
});
|
|
254
|
+
if (onlyYearTokens)
|
|
255
|
+
return [];
|
|
256
|
+
const interpretations = [
|
|
257
|
+
{ text: fullText, year: optionYear, interpretationRank: 0 },
|
|
258
|
+
];
|
|
259
|
+
const inferredYears = optionYear == null ? uniqueYears : uniqueYears.filter((year) => year === optionYear);
|
|
260
|
+
for (const year of inferredYears) {
|
|
261
|
+
const text = normalizeSearchText(query.replace(new RegExp(`\\b${year}\\b`, "g"), " "));
|
|
262
|
+
if (text.spaced.length === 0)
|
|
263
|
+
continue;
|
|
264
|
+
interpretations.push({ text, year: optionYear ?? year, interpretationRank: 1 });
|
|
265
|
+
}
|
|
266
|
+
return interpretations;
|
|
267
|
+
}
|
|
268
|
+
function normalizeLimit(limit) {
|
|
269
|
+
if (limit == null)
|
|
270
|
+
return 10;
|
|
271
|
+
if (!Number.isFinite(limit) || limit <= 0)
|
|
272
|
+
return 0;
|
|
273
|
+
return Math.min(100, Math.floor(limit));
|
|
274
|
+
}
|
|
275
|
+
function matchForms(query, forms, fuzzyOnly) {
|
|
276
|
+
let best;
|
|
277
|
+
for (const form of forms) {
|
|
278
|
+
const match = fuzzyOnly ? fuzzyMatch(query, form) : nonFuzzyMatch(query, form);
|
|
279
|
+
if (match && (!best || compareMatches(match, best) < 0))
|
|
280
|
+
best = match;
|
|
281
|
+
}
|
|
282
|
+
return best;
|
|
283
|
+
}
|
|
284
|
+
function nonFuzzyMatch(query, target) {
|
|
285
|
+
if (query.spaced === target.spaced || query.compact === target.compact) {
|
|
286
|
+
return lexicalMatch("exact", 0);
|
|
287
|
+
}
|
|
288
|
+
if (target.spaced.startsWith(query.spaced) || target.compact.startsWith(query.compact)) {
|
|
289
|
+
return lexicalMatch("prefix", Math.max(0, target.compact.length - query.compact.length));
|
|
290
|
+
}
|
|
291
|
+
const tokenCloseness = tokenPrefixCloseness(query.tokens, target.tokens);
|
|
292
|
+
if (tokenCloseness != null)
|
|
293
|
+
return lexicalMatch("token", tokenCloseness);
|
|
294
|
+
const spacedIndex = target.spaced.indexOf(query.spaced);
|
|
295
|
+
const compactIndex = target.compact.indexOf(query.compact);
|
|
296
|
+
if (spacedIndex >= 0 || compactIndex >= 0) {
|
|
297
|
+
const index = Math.min(spacedIndex < 0 ? Number.MAX_SAFE_INTEGER : spacedIndex, compactIndex < 0 ? Number.MAX_SAFE_INTEGER : compactIndex);
|
|
298
|
+
return lexicalMatch("contains", index + target.compact.length - query.compact.length);
|
|
299
|
+
}
|
|
300
|
+
return undefined;
|
|
301
|
+
}
|
|
302
|
+
function fuzzyMatch(query, target) {
|
|
303
|
+
const fullLimit = allowedEdits(query.compact.length);
|
|
304
|
+
if (fullLimit > 0 && Math.abs(query.compact.length - target.compact.length) <= fullLimit) {
|
|
305
|
+
const distance = damerauLevenshtein(query.compact, target.compact, fullLimit);
|
|
306
|
+
if (distance <= fullLimit)
|
|
307
|
+
return lexicalMatch("fuzzy", distance);
|
|
308
|
+
}
|
|
309
|
+
const tokenDistance = fuzzyTokenDistance(query.tokens, target.tokens);
|
|
310
|
+
return tokenDistance == null ? undefined : lexicalMatch("fuzzy", tokenDistance);
|
|
311
|
+
}
|
|
312
|
+
function lexicalMatch(kind, closeness) {
|
|
313
|
+
return { kind, rank: MATCH_RANK[kind], closeness };
|
|
314
|
+
}
|
|
315
|
+
function compareMatches(left, right) {
|
|
316
|
+
return left.rank - right.rank || left.closeness - right.closeness;
|
|
317
|
+
}
|
|
318
|
+
function tokenPrefixCloseness(queryTokens, targetTokens) {
|
|
319
|
+
if (queryTokens.length === 0 || queryTokens.length > targetTokens.length)
|
|
320
|
+
return undefined;
|
|
321
|
+
const unused = new Set(targetTokens.map((_, index) => index));
|
|
322
|
+
let closeness = 0;
|
|
323
|
+
for (const queryToken of [...queryTokens].sort((left, right) => right.length - left.length)) {
|
|
324
|
+
let bestIndex;
|
|
325
|
+
let bestDifference = Number.MAX_SAFE_INTEGER;
|
|
326
|
+
for (const index of unused) {
|
|
327
|
+
const targetToken = targetTokens[index];
|
|
328
|
+
if (!targetToken.startsWith(queryToken))
|
|
329
|
+
continue;
|
|
330
|
+
const difference = targetToken.length - queryToken.length;
|
|
331
|
+
if (difference < bestDifference) {
|
|
332
|
+
bestDifference = difference;
|
|
333
|
+
bestIndex = index;
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
if (bestIndex == null)
|
|
337
|
+
return undefined;
|
|
338
|
+
unused.delete(bestIndex);
|
|
339
|
+
closeness += bestDifference;
|
|
340
|
+
}
|
|
341
|
+
return closeness;
|
|
342
|
+
}
|
|
343
|
+
function fuzzyTokenDistance(queryTokens, targetTokens) {
|
|
344
|
+
if (queryTokens.length === 0 || queryTokens.length > targetTokens.length)
|
|
345
|
+
return undefined;
|
|
346
|
+
const unused = new Set(targetTokens.map((_, index) => index));
|
|
347
|
+
let totalDistance = 0;
|
|
348
|
+
for (const queryToken of [...queryTokens].sort((left, right) => right.length - left.length)) {
|
|
349
|
+
const limit = allowedEdits(queryToken.length);
|
|
350
|
+
if (limit === 0)
|
|
351
|
+
return undefined;
|
|
352
|
+
let bestIndex;
|
|
353
|
+
let bestDistance = Number.MAX_SAFE_INTEGER;
|
|
354
|
+
for (const index of unused) {
|
|
355
|
+
const targetToken = targetTokens[index];
|
|
356
|
+
if (Math.abs(queryToken.length - targetToken.length) > limit)
|
|
357
|
+
continue;
|
|
358
|
+
const distance = damerauLevenshtein(queryToken, targetToken, limit);
|
|
359
|
+
if (distance < bestDistance) {
|
|
360
|
+
bestDistance = distance;
|
|
361
|
+
bestIndex = index;
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
if (bestIndex == null || bestDistance > limit)
|
|
365
|
+
return undefined;
|
|
366
|
+
unused.delete(bestIndex);
|
|
367
|
+
totalDistance += bestDistance;
|
|
368
|
+
}
|
|
369
|
+
return totalDistance;
|
|
370
|
+
}
|
|
371
|
+
function allowedEdits(length) {
|
|
372
|
+
if (length < 4)
|
|
373
|
+
return 0;
|
|
374
|
+
return length < 6 ? 1 : 2;
|
|
375
|
+
}
|
|
376
|
+
function damerauLevenshtein(left, right, maximum) {
|
|
377
|
+
if (Math.abs(left.length - right.length) > maximum)
|
|
378
|
+
return maximum + 1;
|
|
379
|
+
let previousPrevious = new Array(right.length + 1).fill(0);
|
|
380
|
+
let previous = Array.from({ length: right.length + 1 }, (_, index) => index);
|
|
381
|
+
for (let leftIndex = 1; leftIndex <= left.length; leftIndex++) {
|
|
382
|
+
const current = new Array(right.length + 1).fill(0);
|
|
383
|
+
current[0] = leftIndex;
|
|
384
|
+
for (let rightIndex = 1; rightIndex <= right.length; rightIndex++) {
|
|
385
|
+
const substitutionCost = left[leftIndex - 1] === right[rightIndex - 1] ? 0 : 1;
|
|
386
|
+
current[rightIndex] = Math.min(previous[rightIndex] + 1, current[rightIndex - 1] + 1, previous[rightIndex - 1] + substitutionCost);
|
|
387
|
+
if (leftIndex > 1 &&
|
|
388
|
+
rightIndex > 1 &&
|
|
389
|
+
left[leftIndex - 1] === right[rightIndex - 2] &&
|
|
390
|
+
left[leftIndex - 2] === right[rightIndex - 1]) {
|
|
391
|
+
current[rightIndex] = Math.min(current[rightIndex], previousPrevious[rightIndex - 2] + 1);
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
previousPrevious = previous;
|
|
395
|
+
previous = current;
|
|
396
|
+
}
|
|
397
|
+
return previous[right.length];
|
|
398
|
+
}
|
|
399
|
+
function selectMakeAvailability(rows, year, vehicleTypeId, sourceMask) {
|
|
400
|
+
const years = new Set();
|
|
401
|
+
let combinedSourceMask = 0;
|
|
402
|
+
for (const row of rows) {
|
|
403
|
+
if (year != null && row.year !== year)
|
|
404
|
+
continue;
|
|
405
|
+
if (vehicleTypeId != null && row.vehicleTypeId !== vehicleTypeId)
|
|
406
|
+
continue;
|
|
407
|
+
if (sourceMask != null && (row.sourceMask & sourceMask) === 0)
|
|
408
|
+
continue;
|
|
409
|
+
years.add(row.year);
|
|
410
|
+
combinedSourceMask |= row.sourceMask;
|
|
411
|
+
}
|
|
412
|
+
return { years: [...years].sort((left, right) => left - right), sourceMask: combinedSourceMask };
|
|
413
|
+
}
|
|
414
|
+
function selectModelVariants(rows, year, sourceMask) {
|
|
415
|
+
const variantsByYear = new Map();
|
|
416
|
+
for (const row of rows) {
|
|
417
|
+
const [rowYear, , modelId, modelNameIndex, , rowSourceMask] = row;
|
|
418
|
+
if (year != null && rowYear !== year)
|
|
419
|
+
continue;
|
|
420
|
+
if (sourceMask != null && (rowSourceMask & sourceMask) === 0)
|
|
421
|
+
continue;
|
|
422
|
+
const existing = variantsByYear.get(rowYear);
|
|
423
|
+
const candidate = {
|
|
424
|
+
year: rowYear,
|
|
425
|
+
modelId,
|
|
426
|
+
modelName: data_1.default.modelNames[modelNameIndex],
|
|
427
|
+
sourceMask: rowSourceMask,
|
|
428
|
+
sourceIds: getSourceIds(rowSourceMask),
|
|
429
|
+
};
|
|
430
|
+
if (!existing) {
|
|
431
|
+
variantsByYear.set(rowYear, candidate);
|
|
432
|
+
continue;
|
|
433
|
+
}
|
|
434
|
+
const combinedSourceMask = existing.sourceMask | rowSourceMask;
|
|
435
|
+
const preferred = compareRepresentativeVariants(candidate, existing) < 0 ? candidate : existing;
|
|
436
|
+
variantsByYear.set(rowYear, {
|
|
437
|
+
...preferred,
|
|
438
|
+
sourceMask: combinedSourceMask,
|
|
439
|
+
sourceIds: getSourceIds(combinedSourceMask),
|
|
440
|
+
});
|
|
441
|
+
}
|
|
442
|
+
return [...variantsByYear.values()].sort((left, right) => left.year - right.year);
|
|
443
|
+
}
|
|
444
|
+
function compareRepresentativeVariants(left, right) {
|
|
445
|
+
return (firstSourceIndex(left.sourceMask) - firstSourceIndex(right.sourceMask) ||
|
|
446
|
+
displayNameRank(left.modelName) - displayNameRank(right.modelName) ||
|
|
447
|
+
left.modelName.length - right.modelName.length ||
|
|
448
|
+
compareStrings(left.modelName, right.modelName) ||
|
|
449
|
+
left.modelId - right.modelId);
|
|
450
|
+
}
|
|
451
|
+
function firstSourceIndex(sourceMask) {
|
|
452
|
+
for (let index = 0; index < data_1.default.sources.length; index++) {
|
|
453
|
+
if ((sourceMask & 2 ** index) !== 0)
|
|
454
|
+
return index;
|
|
455
|
+
}
|
|
456
|
+
return Number.MAX_SAFE_INTEGER;
|
|
457
|
+
}
|
|
458
|
+
function displayNameRank(value) {
|
|
459
|
+
return /[a-z]/.test(value) && /[A-Z]/.test(value) ? 0 : 1;
|
|
460
|
+
}
|
|
461
|
+
function getSourceIds(sourceMask) {
|
|
462
|
+
const sourceIds = [];
|
|
463
|
+
for (let index = 0; index < data_1.default.sources.length; index++) {
|
|
464
|
+
if ((sourceMask & 2 ** index) !== 0)
|
|
465
|
+
sourceIds.push(data_1.default.sources[index].source_id);
|
|
466
|
+
}
|
|
467
|
+
return sourceIds;
|
|
468
|
+
}
|
|
469
|
+
function compareRankedResults(left, right) {
|
|
470
|
+
return (left.rank - right.rank ||
|
|
471
|
+
left.interpretationRank - right.interpretationRank ||
|
|
472
|
+
left.closeness - right.closeness ||
|
|
473
|
+
(left.result.kind === right.result.kind ? 0 : left.result.kind === "make" ? -1 : 1) ||
|
|
474
|
+
compareStrings(left.result.makeName, right.result.makeName) ||
|
|
475
|
+
compareStrings(left.result.kind === "model" ? left.result.modelName : "", right.result.kind === "model" ? right.result.modelName : "") ||
|
|
476
|
+
(left.result.kind === "model" ? left.result.vehicleTypeId : 0) -
|
|
477
|
+
(right.result.kind === "model" ? right.result.vehicleTypeId : 0) ||
|
|
478
|
+
compareStrings(left.key, right.key));
|
|
479
|
+
}
|
|
480
|
+
function compareStrings(left, right) {
|
|
481
|
+
return left < right ? -1 : left > right ? 1 : 0;
|
|
482
|
+
}
|
package/package.json
CHANGED
|
@@ -1,13 +1,34 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@meterapp/vehicle-db",
|
|
3
|
-
"version": "2.
|
|
4
|
-
"description": "Offline international
|
|
3
|
+
"version": "2.7.0",
|
|
4
|
+
"description": "Offline international database for cars, motorcycles, trucks, buses, MPVs, and more",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
7
7
|
"url": "git+https://github.com/MeterApp/vehicle-db.git"
|
|
8
8
|
},
|
|
9
9
|
"main": "dist/index.js",
|
|
10
10
|
"types": "dist/index.d.ts",
|
|
11
|
+
"exports": {
|
|
12
|
+
".": {
|
|
13
|
+
"types": "./dist/index.d.ts",
|
|
14
|
+
"require": "./dist/index.js",
|
|
15
|
+
"default": "./dist/index.js"
|
|
16
|
+
},
|
|
17
|
+
"./search": {
|
|
18
|
+
"types": "./dist/search.d.ts",
|
|
19
|
+
"require": "./dist/search.js",
|
|
20
|
+
"default": "./dist/search.js"
|
|
21
|
+
},
|
|
22
|
+
"./package.json": "./package.json"
|
|
23
|
+
},
|
|
24
|
+
"typesVersions": {
|
|
25
|
+
"*": {
|
|
26
|
+
"search": [
|
|
27
|
+
"dist/search.d.ts"
|
|
28
|
+
]
|
|
29
|
+
}
|
|
30
|
+
},
|
|
31
|
+
"sideEffects": false,
|
|
11
32
|
"files": [
|
|
12
33
|
"dist"
|
|
13
34
|
],
|
|
@@ -28,6 +49,9 @@
|
|
|
28
49
|
"nhtsa",
|
|
29
50
|
"dvla",
|
|
30
51
|
"vehicles",
|
|
52
|
+
"vehicle-search",
|
|
53
|
+
"autocomplete",
|
|
54
|
+
"fuzzy-search",
|
|
31
55
|
"makes",
|
|
32
56
|
"models",
|
|
33
57
|
"motorcycles",
|