@meterapp/vehicle-db 2.11.0 → 2.13.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 +68 -15
- package/dist/data.js +1 -1
- package/dist/identity-data.d.ts +3 -0
- package/dist/identity-data.js +4 -0
- package/dist/identity.d.ts +93 -0
- package/dist/identity.js +23 -0
- package/dist/resolve.d.ts +53 -0
- package/dist/resolve.js +127 -0
- package/dist/search.d.ts +2 -0
- package/package.json +25 -4
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
const graph = { "schemaVersion": 1, "revision": "2026-09-24.1", "identities": [{ "id": "bmw:4", "kind": "line", "makeId": 452, "vehicleTypeId": 2, "name": "4 Series", "names": ["4 Series"], "evidenceIds": ["oem:1"] }, { "id": "bmw:4:first", "kind": "generation", "makeId": 452, "vehicleTypeId": 2, "name": "First generation", "names": [], "evidenceIds": ["oem:3"], "parentId": "bmw:4", "years": [2014], "yearBasis": "model-year", "markets": ["US"] }, { "id": "bmw:4:second", "kind": "generation", "makeId": 452, "vehicleTypeId": 2, "name": "Second generation", "names": [], "evidenceIds": ["oem:1"], "parentId": "bmw:4", "years": [2024, 2025], "yearBasis": "model-year", "markets": ["US"] }, { "id": "kia:pegas", "kind": "line", "makeId": 499, "vehicleTypeId": 2, "name": "Pegas", "names": ["Pegas"], "evidenceIds": ["oem:0"], "years": [2025], "yearBasis": "model-year", "markets": ["SA"] }, { "id": "bmw:4:second:coupe", "kind": "derivative", "makeId": 452, "vehicleTypeId": 2, "name": "Coupe", "names": ["4 Series Coupe"], "evidenceIds": ["oem:1", "oem:5"], "years": [2025, 2024], "yearBasis": "model-year", "markets": ["US"], "bodyStyle": "Coupe", "parentId": "bmw:4:second" }, { "id": "bmw:4:second:coupe:2025", "kind": "visual", "makeId": 452, "vehicleTypeId": 2, "name": "4 Series Coupe 2025", "names": [], "evidenceIds": ["oem:1", "oem:5"], "years": [2025], "yearBasis": "model-year", "markets": ["US"], "appearanceRevision": "facelift", "parentId": "bmw:4:second:coupe" }, { "id": "bmw:4:second:convertible", "kind": "derivative", "makeId": 452, "vehicleTypeId": 2, "name": "Convertible", "names": ["4 Series Convertible"], "evidenceIds": ["oem:2", "oem:6"], "years": [2025, 2024], "yearBasis": "model-year", "markets": ["US"], "bodyStyle": "Convertible", "parentId": "bmw:4:second" }, { "id": "bmw:4:second:convertible:2025", "kind": "visual", "makeId": 452, "vehicleTypeId": 2, "name": "4 Series Convertible 2025", "names": [], "evidenceIds": ["oem:2", "oem:6"], "years": [2025], "yearBasis": "model-year", "markets": ["US"], "appearanceRevision": "facelift", "parentId": "bmw:4:second:convertible" }, { "id": "bmw:4:first:coupe", "kind": "derivative", "makeId": 452, "vehicleTypeId": 2, "name": "Coupe", "names": ["4 Series Coupe"], "evidenceIds": ["oem:3"], "years": [2014], "yearBasis": "model-year", "markets": ["US"], "bodyStyle": "Coupe", "parentId": "bmw:4:first" }, { "id": "bmw:4:first:coupe:2014", "kind": "visual", "makeId": 452, "vehicleTypeId": 2, "name": "4 Series Coupe 2014", "names": [], "evidenceIds": ["oem:3"], "years": [2014], "yearBasis": "model-year", "markets": ["US"], "appearanceRevision": "original", "parentId": "bmw:4:first:coupe" }, { "id": "lamborghini:huracan", "kind": "line", "makeId": 502, "vehicleTypeId": 2, "name": "Huracán", "names": ["Huracan"], "evidenceIds": ["oem:4"] }, { "id": "lamborghini:huracan:first", "kind": "generation", "makeId": 502, "vehicleTypeId": 2, "name": "Huracán generation", "names": [], "evidenceIds": ["oem:4"], "parentId": "lamborghini:huracan" }, { "id": "lamborghini:huracan:sto", "kind": "derivative", "makeId": 502, "vehicleTypeId": 2, "name": "STO", "names": ["Huracan STO"], "evidenceIds": ["oem:4"], "years": [2023], "yearBasis": "model-year", "markets": ["US"], "bodyStyle": "Coupe", "parentId": "lamborghini:huracan:first" }, { "id": "lamborghini:huracan:sto:2023", "kind": "visual", "makeId": 502, "vehicleTypeId": 2, "name": "STO exterior", "names": [], "evidenceIds": ["oem:4"], "years": [2023], "yearBasis": "model-year", "markets": ["US"], "parentId": "lamborghini:huracan:sto" }, { "id": "bmw:4:second:coupe:2024", "kind": "visual", "makeId": 452, "vehicleTypeId": 2, "name": "4 Series Coupe 2024", "names": [], "evidenceIds": ["oem:5"], "years": [2024], "yearBasis": "model-year", "markets": ["US"], "appearanceRevision": "original", "parentId": "bmw:4:second:coupe" }, { "id": "bmw:4:second:convertible:2024", "kind": "visual", "makeId": 452, "vehicleTypeId": 2, "name": "4 Series Convertible 2024", "names": [], "evidenceIds": ["oem:6"], "years": [2024], "yearBasis": "model-year", "markets": ["US"], "appearanceRevision": "original", "parentId": "bmw:4:second:convertible" }, { "id": "bmw:4:second:gran-coupe", "kind": "derivative", "makeId": 452, "vehicleTypeId": 2, "name": "Gran Coupe", "names": ["4 Series Gran Coupe"], "evidenceIds": ["oem:7"], "years": [2024], "yearBasis": "model-year", "markets": ["US"], "bodyStyle": "Gran Coupe", "parentId": "bmw:4:second" }, { "id": "bmw:4:second:gran-coupe:2024", "kind": "visual", "makeId": 452, "vehicleTypeId": 2, "name": "4 Series Gran Coupe 2024", "names": [], "evidenceIds": ["oem:7"], "years": [2024], "yearBasis": "model-year", "markets": ["US"], "appearanceRevision": "original", "parentId": "bmw:4:second:gran-coupe" }, { "id": "porsche:macan", "kind": "line", "makeId": 584, "vehicleTypeId": 7, "name": "Macan", "names": ["Macan"], "evidenceIds": ["porsche:macan-name"] }, { "id": "toyota:prado", "kind": "line", "makeId": 448, "vehicleTypeId": 7, "name": "Land Cruiser Prado", "names": ["Land Cruiser Prado"], "evidenceIds": ["toyota:prado-name"] }], "aliases": [{ "text": "maccane", "targetId": "porsche:macan", "kind": "misspelling", "evidenceIds": ["porsche:macan-name"] }, { "text": "Prado", "targetId": "toyota:prado", "kind": "abbreviation", "evidenceIds": ["toyota:prado-name"] }], "catalogLinks": [{ "identityId": "kia:pegas", "year": 2025, "makeId": 499, "modelId": 2554132110, "vehicleTypeId": 2, "evidenceIds": ["oem:0"] }, { "identityId": "bmw:4:second:coupe", "year": 2025, "makeId": 452, "modelId": 1126006782, "vehicleTypeId": 2, "evidenceIds": ["oem:1", "oem:5"] }, { "identityId": "bmw:4:second:convertible", "year": 2025, "makeId": 452, "modelId": 2953619411, "vehicleTypeId": 2, "evidenceIds": ["oem:2", "oem:6"] }, { "identityId": "bmw:4:first:coupe", "year": 2014, "makeId": 452, "modelId": 1126006782, "vehicleTypeId": 2, "evidenceIds": ["oem:3"] }, { "identityId": "lamborghini:huracan:sto", "year": 2023, "makeId": 502, "modelId": 1352298313, "vehicleTypeId": 2, "evidenceIds": ["oem:4"] }, { "identityId": "bmw:4:second:coupe", "year": 2024, "makeId": 452, "modelId": 1126006782, "vehicleTypeId": 2, "evidenceIds": ["oem:5"] }, { "identityId": "bmw:4:second:convertible", "year": 2024, "makeId": 452, "modelId": 2953619411, "vehicleTypeId": 2, "evidenceIds": ["oem:6"] }, { "identityId": "bmw:4:second:gran-coupe", "year": 2024, "makeId": 452, "modelId": 2889216836, "vehicleTypeId": 2, "evidenceIds": ["oem:7"] }, { "identityId": "porsche:macan", "year": 2015, "makeId": 584, "modelId": 8031, "vehicleTypeId": 7, "evidenceIds": ["porsche:macan-name"] }, { "identityId": "porsche:macan", "year": 2016, "makeId": 584, "modelId": 8031, "vehicleTypeId": 7, "evidenceIds": ["porsche:macan-name"] }, { "identityId": "porsche:macan", "year": 2017, "makeId": 584, "modelId": 8031, "vehicleTypeId": 7, "evidenceIds": ["porsche:macan-name"] }, { "identityId": "porsche:macan", "year": 2018, "makeId": 584, "modelId": 8031, "vehicleTypeId": 7, "evidenceIds": ["porsche:macan-name"] }, { "identityId": "porsche:macan", "year": 2019, "makeId": 584, "modelId": 8031, "vehicleTypeId": 7, "evidenceIds": ["porsche:macan-name"] }, { "identityId": "porsche:macan", "year": 2020, "makeId": 584, "modelId": 8031, "vehicleTypeId": 7, "evidenceIds": ["porsche:macan-name"] }, { "identityId": "porsche:macan", "year": 2021, "makeId": 584, "modelId": 8031, "vehicleTypeId": 7, "evidenceIds": ["porsche:macan-name"] }, { "identityId": "porsche:macan", "year": 2022, "makeId": 584, "modelId": 8031, "vehicleTypeId": 7, "evidenceIds": ["porsche:macan-name"] }, { "identityId": "porsche:macan", "year": 2023, "makeId": 584, "modelId": 8031, "vehicleTypeId": 7, "evidenceIds": ["porsche:macan-name"] }, { "identityId": "porsche:macan", "year": 2024, "makeId": 584, "modelId": 8031, "vehicleTypeId": 7, "evidenceIds": ["porsche:macan-name"] }, { "identityId": "porsche:macan", "year": 2025, "makeId": 584, "modelId": 8031, "vehicleTypeId": 7, "evidenceIds": ["porsche:macan-name"] }, { "identityId": "porsche:macan", "year": 2026, "makeId": 584, "modelId": 8031, "vehicleTypeId": 7, "evidenceIds": ["porsche:macan-name"] }, { "identityId": "porsche:macan", "year": 2027, "makeId": 584, "modelId": 8031, "vehicleTypeId": 7, "evidenceIds": ["porsche:macan-name"] }, { "identityId": "toyota:prado", "year": 2015, "makeId": 448, "modelId": 4481618588, "vehicleTypeId": 7, "evidenceIds": ["toyota:prado-name"] }, { "identityId": "toyota:prado", "year": 2016, "makeId": 448, "modelId": 4481618588, "vehicleTypeId": 7, "evidenceIds": ["toyota:prado-name"] }, { "identityId": "toyota:prado", "year": 2017, "makeId": 448, "modelId": 4481618588, "vehicleTypeId": 7, "evidenceIds": ["toyota:prado-name"] }, { "identityId": "toyota:prado", "year": 2018, "makeId": 448, "modelId": 4481618588, "vehicleTypeId": 7, "evidenceIds": ["toyota:prado-name"] }, { "identityId": "toyota:prado", "year": 2019, "makeId": 448, "modelId": 4481618588, "vehicleTypeId": 7, "evidenceIds": ["toyota:prado-name"] }, { "identityId": "toyota:prado", "year": 2020, "makeId": 448, "modelId": 4481618588, "vehicleTypeId": 7, "evidenceIds": ["toyota:prado-name"] }, { "identityId": "toyota:prado", "year": 2021, "makeId": 448, "modelId": 4481618588, "vehicleTypeId": 7, "evidenceIds": ["toyota:prado-name"] }, { "identityId": "toyota:prado", "year": 2022, "makeId": 448, "modelId": 4481618588, "vehicleTypeId": 7, "evidenceIds": ["toyota:prado-name"] }, { "identityId": "toyota:prado", "year": 2023, "makeId": 448, "modelId": 4481618588, "vehicleTypeId": 7, "evidenceIds": ["toyota:prado-name"] }, { "identityId": "toyota:prado", "year": 2024, "makeId": 448, "modelId": 4481618588, "vehicleTypeId": 7, "evidenceIds": ["toyota:prado-name"] }, { "identityId": "toyota:prado", "year": 2025, "makeId": 448, "modelId": 4481618588, "vehicleTypeId": 7, "evidenceIds": ["toyota:prado-name"] }, { "identityId": "toyota:prado", "year": 2026, "makeId": 448, "modelId": 4481618588, "vehicleTypeId": 7, "evidenceIds": ["toyota:prado-name"] }], "evidence": [{ "id": "oem:0", "sourceId": "manufacturer-reviewed", "url": "https://www.kia.com/content/dam/kwcms/sa/en/files/trim/Pegas/LX-D389.pdf", "retrievedAt": "2026-09-24", "note": "Manufacturer specification explicitly labels Pegas 2025 MY; Saudi distributor Aljabr." }, { "id": "oem:1", "sourceId": "manufacturer-reviewed", "url": "https://www.press.bmwgroup.com/usa/article/detail/T0439311EN_US/the-new-2025-bmw-4-series-coupe-and-convertible", "retrievedAt": "2026-09-24", "note": "BMW USA explicitly identifies 2025 model year and the two body derivatives; facelift, not a new generation." }, { "id": "oem:2", "sourceId": "manufacturer-reviewed", "url": "https://www.press.bmwgroup.com/usa/article/detail/T0439311EN_US/the-new-2025-bmw-4-series-coupe-and-convertible", "retrievedAt": "2026-09-24", "note": "BMW USA explicitly identifies 2025 model year and the two body derivatives; facelift, not a new generation." }, { "id": "oem:3", "sourceId": "manufacturer-reviewed", "url": "https://www.press.bmwgroup.com/usa/article/detail/T0142773EN_US/the-all-new-bmw-4-series-coupe", "retrievedAt": "2026-09-24", "note": "BMW USA introduces the first-generation Coupe as model year 2014." }, { "id": "oem:4", "sourceId": "manufacturer-reviewed", "url": "https://preowned.lamborghini.com/en_gb/vdp/30004705-2023-Hurac%C3%A1n-EVO-LP-640-2-STO", "retrievedAt": "2026-09-24", "note": "Lamborghini official pre-owned US dealer listing explicitly identifies model year 2023 STO; no VIN or individual vehicle data retained." }, { "id": "oem:5", "sourceId": "manufacturer-reviewed", "url": "https://bmwtechinfo.bmwgroup.com/sib_attachments/B000223%20Attachment%205%202024%20BMW%20Group%20models%20reference%20guide%209_23.pdf", "retrievedAt": "2026-09-24", "note": "BMW model-year 2024 reference guide lists G22 Coupe, G23 Convertible, and G26 Gran Coupe separately." }, { "id": "oem:6", "sourceId": "manufacturer-reviewed", "url": "https://bmwtechinfo.bmwgroup.com/sib_attachments/B000223%20Attachment%205%202024%20BMW%20Group%20models%20reference%20guide%209_23.pdf", "retrievedAt": "2026-09-24", "note": "BMW model-year 2024 reference guide lists G22 Coupe, G23 Convertible, and G26 Gran Coupe separately." }, { "id": "oem:7", "sourceId": "manufacturer-reviewed", "url": "https://bmwtechinfo.bmwgroup.com/sib_attachments/B000223%20Attachment%205%202024%20BMW%20Group%20models%20reference%20guide%209_23.pdf", "retrievedAt": "2026-09-24", "note": "BMW model-year 2024 reference guide lists G22 Coupe, G23 Convertible, and G26 Gran Coupe separately." }, { "id": "porsche:macan-name", "sourceId": "porsche-catalog", "url": "https://www.porsche.com/usa/models/macan/", "retrievedAt": "2026-09-24", "note": "Macan name evidence. Reviewed maccane spelling correction is lexical only and does not choose gasoline/electric generation." }, { "id": "toyota:prado-name", "sourceId": "toyota-catalog", "url": "https://www.toyota.com.au/prado", "retrievedAt": "2026-09-24", "note": "Toyota uses both Prado and LandCruiser Prado for the model line. No mapping to other Land Cruiser lines or generations." }], "homologation": [], "visualCompatibility": [] };
|
|
4
|
+
exports.default = graph;
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
/** Portable identities. These IDs never replace published numeric catalog IDs. */
|
|
2
|
+
export type IdentityKind = "line" | "generation" | "derivative" | "specification" | "visual";
|
|
3
|
+
export type YearBasis = "model-year" | "manufacture-year" | "registration-year" | "first-admission-year";
|
|
4
|
+
export interface IdentityScope {
|
|
5
|
+
years?: number[];
|
|
6
|
+
markets?: string[];
|
|
7
|
+
yearBasis?: YearBasis;
|
|
8
|
+
}
|
|
9
|
+
export interface IdentityEvidence {
|
|
10
|
+
id: string;
|
|
11
|
+
sourceId: string;
|
|
12
|
+
url: string;
|
|
13
|
+
retrievedAt: string;
|
|
14
|
+
note: string;
|
|
15
|
+
}
|
|
16
|
+
export interface VehicleIdentity extends IdentityScope {
|
|
17
|
+
id: string;
|
|
18
|
+
kind: IdentityKind;
|
|
19
|
+
makeId: number;
|
|
20
|
+
vehicleTypeId: number;
|
|
21
|
+
name: string;
|
|
22
|
+
/** Full model phrases, excluding make/year. No suffix stripping is implied. */
|
|
23
|
+
names: string[];
|
|
24
|
+
parentId?: string;
|
|
25
|
+
codes?: string[];
|
|
26
|
+
bodyStyle?: string;
|
|
27
|
+
appearanceRevision?: string;
|
|
28
|
+
specification?: {
|
|
29
|
+
trim?: string;
|
|
30
|
+
powertrain?: string;
|
|
31
|
+
engine?: string;
|
|
32
|
+
drivetrain?: string;
|
|
33
|
+
transmission?: string;
|
|
34
|
+
rawText: string;
|
|
35
|
+
};
|
|
36
|
+
evidenceIds: string[];
|
|
37
|
+
}
|
|
38
|
+
export interface VehicleIdentityAlias extends IdentityScope {
|
|
39
|
+
text: string;
|
|
40
|
+
targetId: string;
|
|
41
|
+
kind: "marketing-name" | "regional-name" | "translation" | "abbreviation" | "misspelling";
|
|
42
|
+
evidenceIds: string[];
|
|
43
|
+
}
|
|
44
|
+
export interface IdentityCatalogLink {
|
|
45
|
+
identityId: string;
|
|
46
|
+
year: number;
|
|
47
|
+
makeId: number;
|
|
48
|
+
modelId: number;
|
|
49
|
+
vehicleTypeId: number;
|
|
50
|
+
evidenceIds: string[];
|
|
51
|
+
}
|
|
52
|
+
export interface VisualCompatibility extends IdentityScope {
|
|
53
|
+
id: string;
|
|
54
|
+
requestedVisualIdentityId: string;
|
|
55
|
+
assetVisualIdentityId: string;
|
|
56
|
+
decision: "allow" | "deny";
|
|
57
|
+
evidenceIds: string[];
|
|
58
|
+
rationale: string;
|
|
59
|
+
reviewedBy: string;
|
|
60
|
+
reviewedAt: string;
|
|
61
|
+
revision: string;
|
|
62
|
+
}
|
|
63
|
+
export interface HomologationIdentity {
|
|
64
|
+
id: string;
|
|
65
|
+
sourceId: string;
|
|
66
|
+
market: string;
|
|
67
|
+
approvalNumber?: string;
|
|
68
|
+
type?: string;
|
|
69
|
+
variant?: string;
|
|
70
|
+
version?: string;
|
|
71
|
+
derivativeId?: string;
|
|
72
|
+
evidenceIds: string[];
|
|
73
|
+
}
|
|
74
|
+
export interface IdentityGraph {
|
|
75
|
+
schemaVersion: 1;
|
|
76
|
+
revision: string;
|
|
77
|
+
identities: VehicleIdentity[];
|
|
78
|
+
aliases: VehicleIdentityAlias[];
|
|
79
|
+
catalogLinks: IdentityCatalogLink[];
|
|
80
|
+
evidence: IdentityEvidence[];
|
|
81
|
+
homologation: HomologationIdentity[];
|
|
82
|
+
visualCompatibility: VisualCompatibility[];
|
|
83
|
+
}
|
|
84
|
+
export interface ScopeContext {
|
|
85
|
+
year?: number;
|
|
86
|
+
market?: string;
|
|
87
|
+
yearBasis?: YearBasis;
|
|
88
|
+
}
|
|
89
|
+
/** Missing context cannot satisfy a restricted scope. */
|
|
90
|
+
export declare function scopeMatches(scope: IdentityScope, context: ScopeContext): boolean;
|
|
91
|
+
export declare function normalizeIdentityText(text: string): string;
|
|
92
|
+
/** Direct, directional facts only; no reverse or transitive inference. */
|
|
93
|
+
export declare function evaluateVisualCompatibility(rules: readonly VisualCompatibility[], requestedId: string, assetId: string, context: ScopeContext): "allow" | "deny" | "unknown";
|
package/dist/identity.js
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.scopeMatches = scopeMatches;
|
|
4
|
+
exports.normalizeIdentityText = normalizeIdentityText;
|
|
5
|
+
exports.evaluateVisualCompatibility = evaluateVisualCompatibility;
|
|
6
|
+
/** Missing context cannot satisfy a restricted scope. */
|
|
7
|
+
function scopeMatches(scope, context) {
|
|
8
|
+
return (!scope.years || (context.year !== undefined && scope.years.includes(context.year))) &&
|
|
9
|
+
(!scope.markets || (context.market !== undefined && scope.markets.includes(context.market))) &&
|
|
10
|
+
(!scope.yearBasis || scope.yearBasis === context.yearBasis);
|
|
11
|
+
}
|
|
12
|
+
function normalizeIdentityText(text) {
|
|
13
|
+
return text.normalize("NFKD").replace(/[\u0300-\u036f]/g, "").toLowerCase()
|
|
14
|
+
.replace(/[^a-z0-9\p{L}]+/gu, " ").trim().replace(/\s+/g, " ");
|
|
15
|
+
}
|
|
16
|
+
/** Direct, directional facts only; no reverse or transitive inference. */
|
|
17
|
+
function evaluateVisualCompatibility(rules, requestedId, assetId, context) {
|
|
18
|
+
const applicable = rules.filter(rule => rule.requestedVisualIdentityId === requestedId &&
|
|
19
|
+
rule.assetVisualIdentityId === assetId && scopeMatches(rule, context));
|
|
20
|
+
if (applicable.some(rule => rule.decision === "deny"))
|
|
21
|
+
return "deny";
|
|
22
|
+
return applicable.some(rule => rule.decision === "allow") ? "allow" : "unknown";
|
|
23
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { type IdentityGraph, type IdentityKind, type ScopeContext } from "./identity";
|
|
2
|
+
export type { IdentityGraph, VehicleIdentity } from "./identity";
|
|
3
|
+
export interface ResolveVehicleOptions extends ScopeContext {
|
|
4
|
+
makeId?: number;
|
|
5
|
+
vehicleTypeId?: number;
|
|
6
|
+
sourceId?: string;
|
|
7
|
+
/** Default catalog. Rendering consumers should request visual depth. */
|
|
8
|
+
depth?: "catalog" | IdentityKind;
|
|
9
|
+
}
|
|
10
|
+
export interface ResolvedCatalogSelection {
|
|
11
|
+
year: number;
|
|
12
|
+
makeId: number;
|
|
13
|
+
modelId: number;
|
|
14
|
+
modelName: string;
|
|
15
|
+
vehicleTypeId: number;
|
|
16
|
+
sourceIds: string[];
|
|
17
|
+
}
|
|
18
|
+
export interface VehicleResolutionCandidate {
|
|
19
|
+
identityId?: string;
|
|
20
|
+
depth: "catalog" | IdentityKind;
|
|
21
|
+
lineage: Partial<Record<IdentityKind, string>>;
|
|
22
|
+
catalogSelections: ResolvedCatalogSelection[];
|
|
23
|
+
visualIdentityIds: string[];
|
|
24
|
+
evidenceIds: string[];
|
|
25
|
+
}
|
|
26
|
+
export type VehicleResolutionStatus = "MATCHED" | "UNKNOWN_MAKE" | "UNKNOWN_MODEL" | "UNRESOLVED_TOKENS" | "GENERATION_UNRESOLVED" | "AMBIGUOUS" | "YEAR_UNVERIFIED" | "SCOPE_UNVERIFIED" | "KNOWN_DERIVATIVE_UNSUPPORTED" | "INVALID_INPUT";
|
|
27
|
+
export type VehicleResolution = {
|
|
28
|
+
status: "MATCHED";
|
|
29
|
+
selection: VehicleResolutionCandidate;
|
|
30
|
+
candidates: VehicleResolutionCandidate[];
|
|
31
|
+
unresolvedTokens: string[];
|
|
32
|
+
revision: string;
|
|
33
|
+
} | {
|
|
34
|
+
status: Exclude<VehicleResolutionStatus, "MATCHED">;
|
|
35
|
+
candidates: VehicleResolutionCandidate[];
|
|
36
|
+
unresolvedTokens: string[];
|
|
37
|
+
revision: string;
|
|
38
|
+
};
|
|
39
|
+
interface Catalog {
|
|
40
|
+
makes: {
|
|
41
|
+
make_id: number;
|
|
42
|
+
make_name: string;
|
|
43
|
+
}[];
|
|
44
|
+
modelNames: string[];
|
|
45
|
+
models: readonly (readonly number[])[];
|
|
46
|
+
sources: {
|
|
47
|
+
source_id: string;
|
|
48
|
+
}[];
|
|
49
|
+
}
|
|
50
|
+
/** Create an isolated resolver for a validated graph and catalog, including test fixtures. */
|
|
51
|
+
export declare function createVehicleResolver(catalog: Catalog, identities: IdentityGraph): (query: string, options?: ResolveVehicleOptions) => VehicleResolution;
|
|
52
|
+
/** Exact identity resolution, separate from autocomplete and render availability. */
|
|
53
|
+
export declare function resolveVehicle(query: string, options?: ResolveVehicleOptions): VehicleResolution;
|
package/dist/resolve.js
ADDED
|
@@ -0,0 +1,127 @@
|
|
|
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.createVehicleResolver = createVehicleResolver;
|
|
7
|
+
exports.resolveVehicle = resolveVehicle;
|
|
8
|
+
const data_1 = __importDefault(require("./data"));
|
|
9
|
+
const identity_data_1 = __importDefault(require("./identity-data"));
|
|
10
|
+
const identity_1 = require("./identity");
|
|
11
|
+
const rank = { catalog: 0, line: 1, generation: 2, derivative: 3, specification: 4, visual: 5 };
|
|
12
|
+
/** Create an isolated resolver for a validated graph and catalog, including test fixtures. */
|
|
13
|
+
function createVehicleResolver(catalog, identities) {
|
|
14
|
+
const nodes = new Map(identities.identities.map(n => [n.id, n]));
|
|
15
|
+
const ancestry = (node) => {
|
|
16
|
+
const result = [node], seen = new Set([node.id]);
|
|
17
|
+
let parent = node.parentId ? nodes.get(node.parentId) : undefined;
|
|
18
|
+
while (parent && !seen.has(parent.id)) {
|
|
19
|
+
result.push(parent);
|
|
20
|
+
seen.add(parent.id);
|
|
21
|
+
parent = parent.parentId ? nodes.get(parent.parentId) : undefined;
|
|
22
|
+
}
|
|
23
|
+
return result;
|
|
24
|
+
};
|
|
25
|
+
const makeNames = catalog.makes.map(m => ({ id: m.make_id, name: (0, identity_1.normalizeIdentityText)(m.make_name) })).sort((a, b) => b.name.length - a.name.length);
|
|
26
|
+
const byMake = new Map();
|
|
27
|
+
for (const r of catalog.models) {
|
|
28
|
+
const list = byMake.get(r[1]) ?? [];
|
|
29
|
+
list.push(r);
|
|
30
|
+
byMake.set(r[1], list);
|
|
31
|
+
}
|
|
32
|
+
const names = catalog.modelNames.map(identity_1.normalizeIdentityText);
|
|
33
|
+
const select = (r) => ({ year: r[0], makeId: r[1], modelId: r[2], modelName: catalog.modelNames[r[3]], vehicleTypeId: r[4], sourceIds: catalog.sources.filter((_, i) => (r[5] & 2 ** i) !== 0).map(s => s.source_id) });
|
|
34
|
+
return function resolve(query, options = {}) {
|
|
35
|
+
const result = (status, candidates = [], unresolvedTokens = []) => status === "MATCHED"
|
|
36
|
+
? { status, selection: candidates[0], candidates, unresolvedTokens, revision: identities.revision }
|
|
37
|
+
: { status, candidates, unresolvedTokens, revision: identities.revision };
|
|
38
|
+
if (!query.trim() || query.length > 512 || (options.year !== undefined && (!Number.isInteger(options.year) || options.year < 1886 || options.year > 2200)))
|
|
39
|
+
return result("INVALID_INPUT");
|
|
40
|
+
let text = (0, identity_1.normalizeIdentityText)(query), year = options.year;
|
|
41
|
+
// Only a leading year is inferred. Model names such as Peugeot 2008 are preserved.
|
|
42
|
+
const prefix = text.match(/^((?:19|20)\d{2})\s+/);
|
|
43
|
+
if (prefix) {
|
|
44
|
+
if (year !== undefined && year !== Number(prefix[1]))
|
|
45
|
+
return result("INVALID_INPUT");
|
|
46
|
+
year = Number(prefix[1]);
|
|
47
|
+
text = text.slice(prefix[0].length);
|
|
48
|
+
}
|
|
49
|
+
let makeId = options.makeId;
|
|
50
|
+
const make = makeNames.find(m => text === m.name || text.startsWith(m.name + " "));
|
|
51
|
+
if (make && (makeId === undefined || make.id === makeId)) {
|
|
52
|
+
makeId = make.id;
|
|
53
|
+
text = text.slice(make.name.length).trim();
|
|
54
|
+
}
|
|
55
|
+
if (makeId === undefined || !byMake.has(makeId))
|
|
56
|
+
return result("UNKNOWN_MAKE");
|
|
57
|
+
if (!text)
|
|
58
|
+
return result("UNKNOWN_MODEL");
|
|
59
|
+
const context = { year, market: options.market, yearBasis: options.yearBasis ?? "model-year" };
|
|
60
|
+
const sourceIndex = options.sourceId === undefined ? undefined : catalog.sources.findIndex(s => s.source_id === options.sourceId);
|
|
61
|
+
if (sourceIndex === -1)
|
|
62
|
+
return result("SCOPE_UNVERIFIED");
|
|
63
|
+
const rows = (byMake.get(makeId) ?? []).filter(r => (options.vehicleTypeId === undefined || r[4] === options.vehicleTypeId) && (sourceIndex === undefined || (r[5] & 2 ** sourceIndex) !== 0));
|
|
64
|
+
const scopeOk = (n) => ancestry(n).every(p => (0, identity_1.scopeMatches)(p, context));
|
|
65
|
+
const allNamed = identities.identities.filter(n => n.makeId === makeId && (options.vehicleTypeId === undefined || options.vehicleTypeId === n.vehicleTypeId) && n.names.some(s => (0, identity_1.normalizeIdentityText)(s) === text));
|
|
66
|
+
const aliases = identities.aliases.filter(a => (0, identity_1.normalizeIdentityText)(a.text) === text && nodes.get(a.targetId)?.makeId === makeId && (options.vehicleTypeId === undefined || nodes.get(a.targetId)?.vehicleTypeId === options.vehicleTypeId));
|
|
67
|
+
const named = [...new Map([...allNamed, ...aliases.filter(a => (0, identity_1.scopeMatches)(a, context)).map(a => nodes.get(a.targetId))].map(n => [n.id, n])).values()];
|
|
68
|
+
if (allNamed.length || aliases.length) {
|
|
69
|
+
let matching = named.filter(scopeOk);
|
|
70
|
+
if (!matching.length) {
|
|
71
|
+
const otherScopeFits = named.some(n => ancestry(n).every(p => (0, identity_1.scopeMatches)({ ...p, years: undefined }, context)));
|
|
72
|
+
return result(year !== undefined && otherScopeFits ? "YEAR_UNVERIFIED" : "SCOPE_UNVERIFIED");
|
|
73
|
+
}
|
|
74
|
+
const requestedDepth = options.depth ?? "catalog";
|
|
75
|
+
// A line/generation can be completed only when all scope constraints leave one child.
|
|
76
|
+
if (requestedDepth !== "catalog") {
|
|
77
|
+
matching = matching.flatMap(n => rank[n.kind] >= rank[requestedDepth] ? [n] : identities.identities.filter(child => rank[child.kind] === rank[requestedDepth] && ancestry(child).some(p => p.id === n.id) && scopeOk(child)));
|
|
78
|
+
}
|
|
79
|
+
matching = [...new Map(matching.map(n => [n.id, n])).values()];
|
|
80
|
+
if (!matching.length)
|
|
81
|
+
return result("GENERATION_UNRESOLVED");
|
|
82
|
+
const candidates = matching.map(n => {
|
|
83
|
+
const chain = ancestry(n), lineage = Object.fromEntries(chain.map(p => [p.kind, p.id]));
|
|
84
|
+
const links = identities.catalogLinks.filter(l => (l.identityId === n.id || (n.kind === "visual" && chain.some(p => p.id === l.identityId))) && (year === undefined || l.year === year));
|
|
85
|
+
const catalogSelections = rows.filter(r => links.some(l => l.year === r[0] && l.makeId === r[1] && l.modelId === r[2] && l.vehicleTypeId === r[4])).map(select);
|
|
86
|
+
const visuals = identities.identities.filter(v => v.kind === "visual" && ancestry(v).some(p => p.id === n.id) && scopeOk(v));
|
|
87
|
+
return { identityId: n.id, depth: n.kind, lineage, catalogSelections, visualIdentityIds: visuals.map(v => v.id), evidenceIds: [...new Set(chain.flatMap(p => p.evidenceIds))] };
|
|
88
|
+
});
|
|
89
|
+
if (sourceIndex !== undefined && candidates.every(c => !c.catalogSelections.length))
|
|
90
|
+
return result("SCOPE_UNVERIFIED", candidates);
|
|
91
|
+
if (candidates.length !== 1)
|
|
92
|
+
return result("AMBIGUOUS", candidates);
|
|
93
|
+
if (requestedDepth === "catalog" && !candidates[0].catalogSelections.length) {
|
|
94
|
+
const knownLinks = identities.catalogLinks.some(l => l.identityId === candidates[0].identityId);
|
|
95
|
+
return result(knownLinks && year !== undefined ? "YEAR_UNVERIFIED" : "KNOWN_DERIVATIVE_UNSUPPORTED", candidates);
|
|
96
|
+
}
|
|
97
|
+
return result("MATCHED", candidates);
|
|
98
|
+
}
|
|
99
|
+
const exact = rows.filter(r => names[r[3]] === text);
|
|
100
|
+
const yearRows = exact.filter(r => year === undefined || r[0] === year);
|
|
101
|
+
if (exact.length && !yearRows.length)
|
|
102
|
+
return result("YEAR_UNVERIFIED");
|
|
103
|
+
if (yearRows.length) {
|
|
104
|
+
const groups = new Map();
|
|
105
|
+
for (const r of yearRows) {
|
|
106
|
+
const k = `${names[r[3]]}:${r[4]}`;
|
|
107
|
+
const group = groups.get(k) ?? [];
|
|
108
|
+
group.push(r);
|
|
109
|
+
groups.set(k, group);
|
|
110
|
+
}
|
|
111
|
+
const candidates = [...groups.values()].map(rs => ({ depth: "catalog", lineage: {}, catalogSelections: rs.map(select), visualIdentityIds: [], evidenceIds: [] }));
|
|
112
|
+
if (options.market !== undefined)
|
|
113
|
+
return result("SCOPE_UNVERIFIED", candidates);
|
|
114
|
+
if (options.depth && options.depth !== "catalog")
|
|
115
|
+
return result("GENERATION_UNRESOLVED", candidates);
|
|
116
|
+
return result(candidates.length === 1 ? "MATCHED" : "AMBIGUOUS", candidates);
|
|
117
|
+
}
|
|
118
|
+
const prefixes = [...rows.map(r => names[r[3]]), ...identities.identities.filter(n => n.makeId === makeId).flatMap(n => n.names.map(identity_1.normalizeIdentityText))].filter(n => text.startsWith(n + " ")).sort((a, b) => b.length - a.length);
|
|
119
|
+
return prefixes.length ? result("UNRESOLVED_TOKENS", [], text.slice(prefixes[0].length).trim().split(" ")) : result("UNKNOWN_MODEL");
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
let resolver;
|
|
123
|
+
/** Exact identity resolution, separate from autocomplete and render availability. */
|
|
124
|
+
function resolveVehicle(query, options = {}) {
|
|
125
|
+
resolver ?? (resolver = createVehicleResolver(data_1.default, identity_data_1.default));
|
|
126
|
+
return resolver(query, options);
|
|
127
|
+
}
|
package/dist/search.d.ts
CHANGED
|
@@ -26,6 +26,8 @@ export interface VehicleSearchVariant {
|
|
|
26
26
|
modelId: number;
|
|
27
27
|
sourceIds: string[];
|
|
28
28
|
}
|
|
29
|
+
/** Clearer name for the existing year-specific selection; variants remains compatible. */
|
|
30
|
+
export type VehicleSearchSelection = VehicleSearchVariant;
|
|
29
31
|
export interface VehicleModelSearchResult extends VehicleSearchResultBase {
|
|
30
32
|
kind: "model";
|
|
31
33
|
modelName: string;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@meterapp/vehicle-db",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.13.0",
|
|
4
4
|
"description": "Offline international database for cars, motorcycles, trucks, buses, MPVs, and more",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -23,12 +23,28 @@
|
|
|
23
23
|
"require": "./dist/search.js",
|
|
24
24
|
"default": "./dist/search.js"
|
|
25
25
|
},
|
|
26
|
-
"./package.json": "./package.json"
|
|
26
|
+
"./package.json": "./package.json",
|
|
27
|
+
"./resolve": {
|
|
28
|
+
"types": "./dist/resolve.d.ts",
|
|
29
|
+
"require": "./dist/resolve.js",
|
|
30
|
+
"default": "./dist/resolve.js"
|
|
31
|
+
},
|
|
32
|
+
"./identity": {
|
|
33
|
+
"types": "./dist/identity.d.ts",
|
|
34
|
+
"require": "./dist/identity.js",
|
|
35
|
+
"default": "./dist/identity.js"
|
|
36
|
+
}
|
|
27
37
|
},
|
|
28
38
|
"typesVersions": {
|
|
29
39
|
"*": {
|
|
30
40
|
"search": [
|
|
31
41
|
"dist/search.d.ts"
|
|
42
|
+
],
|
|
43
|
+
"resolve": [
|
|
44
|
+
"dist/resolve.d.ts"
|
|
45
|
+
],
|
|
46
|
+
"identity": [
|
|
47
|
+
"dist/identity.d.ts"
|
|
32
48
|
]
|
|
33
49
|
}
|
|
34
50
|
},
|
|
@@ -38,7 +54,7 @@
|
|
|
38
54
|
],
|
|
39
55
|
"scripts": {
|
|
40
56
|
"build:db": "npm run build:data",
|
|
41
|
-
"build:data": "tsx scripts/build-db.ts",
|
|
57
|
+
"build:data": "tsx scripts/build-db.ts && tsx scripts/build-identities.ts",
|
|
42
58
|
"refresh:nhtsa": "tsx scripts/import-nhtsa.ts",
|
|
43
59
|
"refresh:uk-dft": "tsx scripts/import-uk-dft.ts",
|
|
44
60
|
"refresh:nzta-asia-pacific": "tsx scripts/import-nzta-asia-pacific.ts",
|
|
@@ -49,7 +65,12 @@
|
|
|
49
65
|
"build": "npm run build:ts",
|
|
50
66
|
"typecheck": "tsc --noEmit && tsc -p tsconfig.scripts.json",
|
|
51
67
|
"prepublishOnly": "npm run build",
|
|
52
|
-
"test": "vitest run"
|
|
68
|
+
"test": "vitest run",
|
|
69
|
+
"build:identities": "tsx scripts/build-identities.ts",
|
|
70
|
+
"check:compatibility": "tsx scripts/check-compatibility.ts",
|
|
71
|
+
"refresh:fueleconomy-us": "tsx scripts/import-fueleconomy-us.ts",
|
|
72
|
+
"refresh:reviewed": "tsx scripts/import-reviewed-models.ts data/evidence/manufacturer-reviewed.json",
|
|
73
|
+
"replay": "tsx scripts/replay-catalog.ts"
|
|
53
74
|
},
|
|
54
75
|
"keywords": [
|
|
55
76
|
"nhtsa",
|