@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/dist/index.d.ts CHANGED
@@ -39,8 +39,61 @@ export interface GetModelsOptions {
39
39
  year?: number;
40
40
  vehicleTypeId?: number;
41
41
  makeId?: number;
42
+ modelId?: number;
42
43
  sourceId?: string;
43
44
  }
45
+ export interface GetAvailableYearsOptions {
46
+ vehicleTypeId?: number;
47
+ makeId?: number;
48
+ modelId?: number;
49
+ sourceId?: string;
50
+ }
51
+ export interface VehicleYearRange {
52
+ yearFrom: number;
53
+ yearTo: number;
54
+ }
55
+ /**
56
+ * A sourced period in which a model shares one exterior design. Ranges may
57
+ * overlap when a facelift, body variant, or regional model changed during a
58
+ * calendar year; callers must not treat an overlap as render-equivalent.
59
+ */
60
+ export interface VehicleAppearanceRange {
61
+ appearanceId: string;
62
+ makeId: number;
63
+ makeName: string;
64
+ modelName: string;
65
+ label: string;
66
+ bodyStyle: string;
67
+ yearFrom: number;
68
+ yearTo: number;
69
+ representativeYear: number;
70
+ regions: string[];
71
+ sourceName: string;
72
+ sourceUrl: string;
73
+ }
74
+ export interface GetModelAppearanceRangesOptions {
75
+ makeId: number;
76
+ modelName: string;
77
+ year?: number;
78
+ }
79
+ export interface GetModelRenderGroupsOptions extends GetModelAppearanceRangesOptions {
80
+ vehicleTypeId?: number;
81
+ sourceId?: string;
82
+ }
83
+ /**
84
+ * Safe cache groups for every available year. Sourced, unambiguous appearance
85
+ * periods are grouped; unknown and overlapping years deliberately stay as
86
+ * singleton exact-year groups.
87
+ */
88
+ export interface VehicleRenderGroup {
89
+ groupId: string;
90
+ kind: "appearance" | "year";
91
+ years: number[];
92
+ yearFrom: number;
93
+ yearTo: number;
94
+ representativeYear: number;
95
+ appearance?: VehicleAppearanceRange;
96
+ }
44
97
  /**
45
98
  * Returns all vehicle types in the database.
46
99
  */
@@ -58,6 +111,26 @@ export declare function getMakes(options?: GetMakesOptions): Make[];
58
111
  */
59
112
  export declare function getModels(options?: GetModelsOptions): Model[];
60
113
  /**
61
- * Get all available years in the database.
114
+ * Gets available years, optionally filtered by vehicle identity and source.
115
+ */
116
+ export declare function getAvailableYears(options?: GetAvailableYearsOptions): number[];
117
+ /**
118
+ * Compresses available catalog years into contiguous ranges. These ranges
119
+ * describe availability only; use getModelAppearanceRanges when exterior
120
+ * equivalence matters.
121
+ */
122
+ export declare function getAvailableYearRanges(options?: GetAvailableYearsOptions): VehicleYearRange[];
123
+ /** Returns the curated, source-backed exterior-design periods for a model. */
124
+ export declare function getModelAppearanceRanges(options: GetModelAppearanceRangesOptions): VehicleAppearanceRange[];
125
+ /**
126
+ * Returns a complete, non-overlapping render plan for a model. A year is only
127
+ * grouped when exactly one verified appearance range contains it.
128
+ */
129
+ export declare function getModelRenderGroups(options: GetModelRenderGroupsOptions): VehicleRenderGroup[];
130
+ /**
131
+ * Canonical year for cache/storage identity. Unknown, unavailable, and
132
+ * ambiguous years return themselves, which is the fail-closed behavior.
62
133
  */
63
- export declare function getAvailableYears(): number[];
134
+ export declare function getRepresentativeYear(options: GetModelRenderGroupsOptions & {
135
+ year: number;
136
+ }): number;
package/dist/index.js CHANGED
@@ -8,6 +8,10 @@ exports.getDataSources = getDataSources;
8
8
  exports.getMakes = getMakes;
9
9
  exports.getModels = getModels;
10
10
  exports.getAvailableYears = getAvailableYears;
11
+ exports.getAvailableYearRanges = getAvailableYearRanges;
12
+ exports.getModelAppearanceRanges = getModelAppearanceRanges;
13
+ exports.getModelRenderGroups = getModelRenderGroups;
14
+ exports.getRepresentativeYear = getRepresentativeYear;
11
15
  const data_1 = __importDefault(require("./data"));
12
16
  // ---------------------------------------------------------------------------
13
17
  // Internal lookup maps (lazy)
@@ -41,6 +45,56 @@ function getSourceIds(sourceMask) {
41
45
  }
42
46
  return sourceIds;
43
47
  }
48
+ function normalizeLookupName(value) {
49
+ return value.trim().toLocaleUpperCase("en-US").replace(/\s+/g, " ");
50
+ }
51
+ function compactYears(years) {
52
+ const sorted = [...new Set(years)].sort((left, right) => left - right);
53
+ const ranges = [];
54
+ for (const year of sorted) {
55
+ const previous = ranges[ranges.length - 1];
56
+ if (previous && previous.yearTo + 1 === year)
57
+ previous.yearTo = year;
58
+ else
59
+ ranges.push({ yearFrom: year, yearTo: year });
60
+ }
61
+ return ranges;
62
+ }
63
+ function modelNameYears(options) {
64
+ const sourceMask = options.sourceId == null ? undefined : getSourceMaskMap().get(options.sourceId);
65
+ if (options.sourceId != null && sourceMask == null)
66
+ return [];
67
+ const wantedModel = normalizeLookupName(options.modelName);
68
+ const years = new Set();
69
+ for (const model of data_1.default.models) {
70
+ if (model[1] !== options.makeId)
71
+ continue;
72
+ if (options.vehicleTypeId != null && model[4] !== options.vehicleTypeId)
73
+ continue;
74
+ if (sourceMask != null && (model[5] & sourceMask) === 0)
75
+ continue;
76
+ if (normalizeLookupName(data_1.default.modelNames[model[3]]) !== wantedModel)
77
+ continue;
78
+ years.add(model[0]);
79
+ }
80
+ return [...years].sort((left, right) => left - right);
81
+ }
82
+ function toAppearanceRange(range) {
83
+ return {
84
+ appearanceId: range.id,
85
+ makeId: range.make_id,
86
+ makeName: range.make_name,
87
+ modelName: range.model_name,
88
+ label: range.label,
89
+ bodyStyle: range.body_style,
90
+ yearFrom: range.year_from,
91
+ yearTo: range.year_to,
92
+ representativeYear: range.representative_year,
93
+ regions: [...range.regions],
94
+ sourceName: range.source_name,
95
+ sourceUrl: range.source_url,
96
+ };
97
+ }
44
98
  // ---------------------------------------------------------------------------
45
99
  // Public API
46
100
  // ---------------------------------------------------------------------------
@@ -105,7 +159,7 @@ function getMakes(options = {}) {
105
159
  function getModels(options = {}) {
106
160
  const makeMap = getMakeMap();
107
161
  const typeMap = getTypeMap();
108
- const { year, vehicleTypeId, makeId, sourceId } = options;
162
+ const { year, vehicleTypeId, makeId, modelId, sourceId } = options;
109
163
  const sourceMask = sourceId == null ? undefined : getSourceMaskMap().get(sourceId);
110
164
  if (sourceId != null && sourceMask == null)
111
165
  return [];
@@ -115,6 +169,8 @@ function getModels(options = {}) {
115
169
  continue;
116
170
  if (makeId != null && m[1] !== makeId)
117
171
  continue;
172
+ if (modelId != null && m[2] !== modelId)
173
+ continue;
118
174
  if (vehicleTypeId != null && m[4] !== vehicleTypeId)
119
175
  continue;
120
176
  if (sourceMask != null && (m[5] & sourceMask) === 0)
@@ -133,12 +189,91 @@ function getModels(options = {}) {
133
189
  a.vehicleTypeId - b.vehicleTypeId);
134
190
  }
135
191
  /**
136
- * Get all available years in the database.
192
+ * Gets available years, optionally filtered by vehicle identity and source.
137
193
  */
138
- function getAvailableYears() {
194
+ function getAvailableYears(options = {}) {
195
+ const { vehicleTypeId, makeId, modelId, sourceId } = options;
196
+ const sourceMask = sourceId == null ? undefined : getSourceMaskMap().get(sourceId);
197
+ if (sourceId != null && sourceMask == null)
198
+ return [];
139
199
  const years = new Set();
140
200
  for (const m of data_1.default.models) {
201
+ if (makeId != null && m[1] !== makeId)
202
+ continue;
203
+ if (modelId != null && m[2] !== modelId)
204
+ continue;
205
+ if (vehicleTypeId != null && m[4] !== vehicleTypeId)
206
+ continue;
207
+ if (sourceMask != null && (m[5] & sourceMask) === 0)
208
+ continue;
141
209
  years.add(m[0]);
142
210
  }
143
211
  return [...years].sort((a, b) => a - b);
144
212
  }
213
+ /**
214
+ * Compresses available catalog years into contiguous ranges. These ranges
215
+ * describe availability only; use getModelAppearanceRanges when exterior
216
+ * equivalence matters.
217
+ */
218
+ function getAvailableYearRanges(options = {}) {
219
+ return compactYears(getAvailableYears(options));
220
+ }
221
+ /** Returns the curated, source-backed exterior-design periods for a model. */
222
+ function getModelAppearanceRanges(options) {
223
+ const wantedModel = normalizeLookupName(options.modelName);
224
+ return data_1.default.appearanceRanges
225
+ .filter((range) => range.make_id === options.makeId &&
226
+ normalizeLookupName(range.model_name) === wantedModel &&
227
+ (options.year == null ||
228
+ (range.year_from <= options.year && range.year_to >= options.year)))
229
+ .map(toAppearanceRange);
230
+ }
231
+ /**
232
+ * Returns a complete, non-overlapping render plan for a model. A year is only
233
+ * grouped when exactly one verified appearance range contains it.
234
+ */
235
+ function getModelRenderGroups(options) {
236
+ const years = modelNameYears(options);
237
+ const appearances = getModelAppearanceRanges(options);
238
+ const groupYears = new Map();
239
+ const appearanceByGroup = new Map();
240
+ for (const year of years) {
241
+ const candidates = appearances.filter((appearance) => appearance.yearFrom <= year && appearance.yearTo >= year);
242
+ const groupId = candidates.length === 1 ? candidates[0].appearanceId : `year:${year}`;
243
+ const existing = groupYears.get(groupId);
244
+ if (existing)
245
+ existing.push(year);
246
+ else
247
+ groupYears.set(groupId, [year]);
248
+ if (candidates.length === 1)
249
+ appearanceByGroup.set(groupId, candidates[0]);
250
+ }
251
+ return [...groupYears.entries()]
252
+ .map(([groupId, groupedYears]) => {
253
+ const appearance = appearanceByGroup.get(groupId);
254
+ const preferred = appearance?.representativeYear;
255
+ const representativeYear = preferred != null && groupedYears.includes(preferred)
256
+ ? preferred
257
+ : groupedYears.reduce((best, year) => preferred != null && Math.abs(year - preferred) < Math.abs(best - preferred)
258
+ ? year
259
+ : best);
260
+ return {
261
+ groupId,
262
+ kind: appearance ? "appearance" : "year",
263
+ years: [...groupedYears],
264
+ yearFrom: groupedYears[0],
265
+ yearTo: groupedYears[groupedYears.length - 1],
266
+ representativeYear,
267
+ ...(appearance ? { appearance } : {}),
268
+ };
269
+ })
270
+ .sort((left, right) => left.yearFrom - right.yearFrom || left.yearTo - right.yearTo);
271
+ }
272
+ /**
273
+ * Canonical year for cache/storage identity. Unknown, unavailable, and
274
+ * ambiguous years return themselves, which is the fail-closed behavior.
275
+ */
276
+ function getRepresentativeYear(options) {
277
+ const group = getModelRenderGroups(options).find((candidate) => candidate.years.includes(options.year));
278
+ return group?.representativeYear ?? options.year;
279
+ }
@@ -0,0 +1,47 @@
1
+ export type VehicleSearchMatchKind = "exact" | "prefix" | "token" | "contains" | "fuzzy";
2
+ export interface SearchVehiclesOptions {
3
+ /** Restrict results to one catalog year. A year in the query is detected automatically. */
4
+ year?: number;
5
+ vehicleTypeId?: number;
6
+ sourceId?: string;
7
+ /** Maximum number of results. Defaults to 10 and is capped at 100. */
8
+ limit?: number;
9
+ /** Enables conservative typo matching. Defaults to true. */
10
+ fuzzy?: boolean;
11
+ }
12
+ interface VehicleSearchResultBase {
13
+ makeId: number;
14
+ makeName: string;
15
+ /** Available years after applying the supplied filters, sorted ascending. */
16
+ years: number[];
17
+ sourceIds: string[];
18
+ matchKind: VehicleSearchMatchKind;
19
+ }
20
+ export interface VehicleMakeSearchResult extends VehicleSearchResultBase {
21
+ kind: "make";
22
+ }
23
+ export interface VehicleSearchVariant {
24
+ year: number;
25
+ /** The preferred catalog model ID for this canonical make/model/type/year. */
26
+ modelId: number;
27
+ sourceIds: string[];
28
+ }
29
+ export interface VehicleModelSearchResult extends VehicleSearchResultBase {
30
+ kind: "model";
31
+ modelName: string;
32
+ vehicleTypeId: number;
33
+ vehicleTypeName: string;
34
+ /** One directly selectable catalog identity per available year. */
35
+ variants: VehicleSearchVariant[];
36
+ }
37
+ export type VehicleSearchResult = VehicleMakeSearchResult | VehicleModelSearchResult;
38
+ /**
39
+ * Finds canonical make and model candidates for a driver-entered query.
40
+ *
41
+ * Matching is deterministic and popularity-neutral: exact, prefix, token,
42
+ * substring, then conservative typo matching. Applications can request a
43
+ * larger candidate set and rerank within the returned `matchKind` using local
44
+ * recents or regional telemetry.
45
+ */
46
+ export declare function searchVehicles(query: string, options?: SearchVehiclesOptions): VehicleSearchResult[];
47
+ export {};