@mleonard9/vin-scanner 1.2.0 → 1.2.2

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/src/vinUtils.ts CHANGED
@@ -54,9 +54,8 @@ const DEFAULT_RESOLVED_OPTIONS: ResolvedVinScannerOptions = {
54
54
  language: 'latin',
55
55
  },
56
56
  detection: {
57
- resultMode: 'best',
57
+ resultMode: 'first',
58
58
  preferBarcode: true,
59
- validateChecksum: true,
60
59
  emitDuplicates: false,
61
60
  maxFrameRate: 30,
62
61
  forceOrientation: null,
@@ -94,9 +93,6 @@ export const resolveOptions = (
94
93
  preferBarcode:
95
94
  options?.detection?.preferBarcode ??
96
95
  DEFAULT_RESOLVED_OPTIONS.detection.preferBarcode,
97
- validateChecksum:
98
- options?.detection?.validateChecksum ??
99
- DEFAULT_RESOLVED_OPTIONS.detection.validateChecksum,
100
96
  emitDuplicates,
101
97
  maxFrameRate:
102
98
  options?.detection?.maxFrameRate ??
@@ -320,12 +316,16 @@ const isValidNorthAmericaWmi = (value: string): boolean => {
320
316
  return false;
321
317
  }
322
318
  const firstChar = value[0];
323
- // North America: 1, 2 (Canada), 4, 5 (United States)
319
+ // North America: 1, 2 (Canada), 3, 4, 5 (United States)
320
+ // Japan : 'J' Germany : 'W'
324
321
  return (
325
322
  firstChar === '1' ||
326
323
  firstChar === '2' ||
324
+ firstChar === '3' ||
327
325
  firstChar === '4' ||
328
- firstChar === '5'
326
+ firstChar === '5' ||
327
+ firstChar === 'J' ||
328
+ firstChar === 'W'
329
329
  );
330
330
  };
331
331
 
@@ -466,34 +466,7 @@ const toBoundingBox = (
466
466
  return blockBox || lineBox || elementBox;
467
467
  };
468
468
 
469
- /**
470
- * Calculate confidence score for a barcode-based VIN candidate
471
- * Barcodes/QR codes are hard values, so they should always return 100% confidence
472
- */
473
- const calculateBarcodeConfidence = (
474
- value: string,
475
- validateChecksum: boolean
476
- ): number => {
477
- 'worklet';
478
- // Barcodes/QR codes are hard values - always 100% confidence for valid VINs
479
- const checksumValid = isValidVin(value);
480
- if (checksumValid) {
481
- return 1.0; // 100% confidence for valid VINs from barcodes
482
- }
483
-
484
- // If validation is disabled, still return high confidence
485
- if (!validateChecksum) {
486
- return 0.95;
487
- }
488
-
489
- // Invalid VINs should not be returned (handled by caller)
490
- return 0;
491
- };
492
-
493
- const candidateFromBarcode = (
494
- detection: BarcodeDetection,
495
- validateChecksum: boolean
496
- ): VinCandidate[] => {
469
+ const candidateFromBarcode = (detection: BarcodeDetection): VinCandidate[] => {
497
470
  'worklet';
498
471
  const values = new Set<string>();
499
472
  tokenizeCandidate(detection.rawValue ?? '').forEach((value) =>
@@ -504,73 +477,17 @@ const candidateFromBarcode = (
504
477
  );
505
478
  const boundingBox = toBoundingBox(detection, 'barcode');
506
479
  return Array.from(values)
507
- .filter((value) => {
508
- // Only return valid full VINs (17 characters that pass validation)
509
- if (validateChecksum) {
510
- return isValidVin(value);
511
- }
512
- // If validation is disabled, still only return 17-character VINs
513
- return value.length === 17;
514
- })
515
- .map((value) => {
516
- const confidence = calculateBarcodeConfidence(value, validateChecksum);
517
- return {
518
- value,
519
- source: 'barcode' as const,
520
- confidence,
521
- boundingBox,
522
- origin: detection.displayValue ? 'displayValue' : 'rawValue',
523
- rawPayload: detection,
524
- };
525
- });
480
+ .filter((value) => isValidVin(value))
481
+ .map((value) => ({
482
+ value,
483
+ source: 'barcode' as const,
484
+ boundingBox,
485
+ origin: detection.displayValue ? 'displayValue' : 'rawValue',
486
+ rawPayload: detection,
487
+ }));
526
488
  };
527
489
 
528
- /**
529
- * Calculate confidence score for a text-based VIN candidate
530
- * Higher confidence for:
531
- * - Valid check digit
532
- * - Found with VIN prefix context
533
- * - Element/line level detection (vs block level)
534
- */
535
- const calculateTextConfidence = (
536
- value: string,
537
- rawText: string,
538
- origin: string,
539
- validateChecksum: boolean
540
- ): number => {
541
- 'worklet';
542
- let confidence = 0.5; // Base confidence for text recognition
543
-
544
- // Check digit validation significantly increases confidence
545
- const checksumValid = isValidVin(value);
546
- if (checksumValid) {
547
- confidence += 0.35; // Boost to 0.85
548
- } else if (!validateChecksum) {
549
- confidence += 0.2; // Still boost if validation is disabled
550
- }
551
-
552
- // Context-aware detection (VIN prefix found) increases confidence
553
- const hasVinPrefix =
554
- /VIN|Vehicle\s+(?:Identification|ID|Id)|Chassis|Serial/i.test(rawText);
555
- if (hasVinPrefix) {
556
- confidence += 0.1; // Boost to 0.95 if checksum valid, 0.6 if not
557
- }
558
-
559
- // Element-level text is more accurate than block-level
560
- if (origin === 'element') {
561
- confidence += 0.05;
562
- } else if (origin === 'line') {
563
- confidence += 0.03;
564
- }
565
-
566
- // Cap at 0.98 (never 1.0 for text recognition)
567
- return Math.min(confidence, 0.98);
568
- };
569
-
570
- const candidateFromText = (
571
- detection: TextDetection,
572
- validateChecksum: boolean
573
- ): VinCandidate[] => {
490
+ const candidateFromText = (detection: TextDetection): VinCandidate[] => {
574
491
  'worklet';
575
492
  const valueMap = new Map<string, { rawText: string; order: number }>();
576
493
 
@@ -596,32 +513,18 @@ const candidateFromText = (
596
513
  toBoundingBox(detection, 'block');
597
514
 
598
515
  return Array.from(valueMap.entries())
599
- .filter(([value]) => {
600
- // Only return valid full VINs (17 characters that pass validation)
601
- if (validateChecksum) {
602
- return isValidVin(value);
603
- }
604
- // If validation is disabled, still only return 17-character VINs
605
- return value.length === 17;
606
- })
607
- .map(([value, { rawText }]) => {
516
+ .map(([value]) => value)
517
+ .filter((value) => isValidVin(value))
518
+ .map((value) => {
608
519
  const origin = detection.elementText?.includes(value)
609
520
  ? 'element'
610
521
  : detection.lineText?.includes(value)
611
522
  ? 'line'
612
523
  : 'block';
613
524
 
614
- const confidence = calculateTextConfidence(
615
- value,
616
- rawText,
617
- origin,
618
- validateChecksum
619
- );
620
-
621
525
  return {
622
526
  value,
623
527
  source: 'text' as const,
624
- confidence,
625
528
  boundingBox,
626
529
  origin,
627
530
  rawPayload: detection,
@@ -639,8 +542,7 @@ const dedupeCandidates = (candidates: VinCandidate[]): VinCandidate[] => {
639
542
  candidate.boundingBox?.top ?? 'x',
640
543
  candidate.boundingBox?.left ?? 'y',
641
544
  ].join(':');
642
- const existing = map.get(key);
643
- if (!existing || existing.confidence < candidate.confidence) {
545
+ if (!map.has(key)) {
644
546
  map.set(key, candidate);
645
547
  }
646
548
  });
@@ -654,39 +556,43 @@ export const buildVinCandidates = (
654
556
  'worklet';
655
557
  const list: VinCandidate[] = [];
656
558
 
657
- if (options.barcode.enabled) {
559
+ const addBarcodeCandidates = () => {
560
+ if (!options.barcode.enabled) {
561
+ return;
562
+ }
658
563
  (payload.barcodes ?? []).forEach((barcode) => {
659
- list.push(
660
- ...candidateFromBarcode(barcode, options.detection.validateChecksum)
661
- );
564
+ list.push(...candidateFromBarcode(barcode));
662
565
  });
663
- }
664
- if (options.text.enabled) {
566
+ };
567
+
568
+ const addTextCandidates = () => {
569
+ if (!options.text.enabled) {
570
+ return;
571
+ }
665
572
  (payload.textBlocks ?? []).forEach((textBlock) => {
666
- list.push(
667
- ...candidateFromText(textBlock, options.detection.validateChecksum)
668
- );
573
+ list.push(...candidateFromText(textBlock));
669
574
  });
575
+ };
576
+
577
+ if (options.detection.preferBarcode) {
578
+ addBarcodeCandidates();
579
+ addTextCandidates();
580
+ } else {
581
+ addTextCandidates();
582
+ addBarcodeCandidates();
670
583
  }
671
584
  return dedupeCandidates(list);
672
585
  };
673
586
 
674
- export const pickBestCandidate = (
587
+ export const pickFirstCandidate = (
675
588
  candidates: VinCandidate[],
676
589
  options: ResolvedVinScannerOptions
677
590
  ): VinCandidate | null => {
678
591
  'worklet';
592
+ // eslint-disable-next-line no-void
593
+ void options;
679
594
  if (!candidates.length) {
680
595
  return null;
681
596
  }
682
- const sorted = [...candidates].sort((a, b) => {
683
- if (a.confidence !== b.confidence) {
684
- return b.confidence - a.confidence;
685
- }
686
- if (options.detection.preferBarcode && a.source !== b.source) {
687
- return a.source === 'barcode' ? -1 : 1;
688
- }
689
- return 0;
690
- });
691
- return sorted[0] ?? null;
597
+ return candidates[0] ?? null;
692
598
  };