hh-shipments 0.3.27 → 0.3.28

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/package.json CHANGED
@@ -19,7 +19,7 @@
19
19
  "test": "node test/run-extract-tests.js && node test/run-tests.js && node test/run-carriers-tests.js"
20
20
  },
21
21
  "type": "commonjs",
22
- "version": "0.3.27",
22
+ "version": "0.3.28",
23
23
  "bin": {
24
24
  "hh-shipments": "server.js"
25
25
  },
package/public/app.js CHANGED
@@ -450,6 +450,18 @@ async function decodeOne(reader, url, ms = 9000) {
450
450
  } catch (e) { return null; }
451
451
  }
452
452
 
453
+ // Rank barcode candidates by format specificity: a 1Z UPS read or a
454
+ // 22-digit USPS read is unambiguous, while a 10-digit "DHL" read off a
455
+ // label is often a fragment of another carrier's number (or a routing
456
+ // barcode). Weak hits never beat strong ones.
457
+ function barcodeRank(num) {
458
+ if (/^1Z/i.test(num)) return 0; // UPS
459
+ if (/^9[1-5]\d{18,20}$/.test(num)) return 1; // USPS
460
+ if (/^96\d{20}$/.test(num) || /^\d{15}$/.test(num) || /^\d{20}$/.test(num)) return 2; // FedEx long
461
+ if (/^\d{12}$/.test(num)) return 3; // FedEx 12 (weak: may be routing)
462
+ return 4; // DHL 10/11 (weakest)
463
+ }
464
+
453
465
  // Whole-label decode usually fails (barcodes too small in frame), so try
454
466
  // horizontal region strips, upscaled — the 1Z barcode lives low on UPS labels.
455
467
  async function decodeBarcode(file) {
@@ -467,6 +479,11 @@ async function decodeBarcode(file) {
467
479
  [0, 0.60, 1, 0.35],
468
480
  [0.05, 0.55, 0.90, 0.40],
469
481
  ];
482
+ // Collect every candidate across the sweep and take the strongest —
483
+ // never let the first weak hit (e.g. a 10-digit routing barcode read as
484
+ // DHL) win over the real tracking barcode. Unambiguous reads (UPS/USPS)
485
+ // still return immediately on the fast path.
486
+ let best = null, bestRank = 99;
470
487
  try {
471
488
  for (const [fx, fy, fw, fh] of regions) {
472
489
  for (const scale of [1, 1.5, 2]) {
@@ -491,11 +508,15 @@ async function decodeBarcode(file) {
491
508
  const url = canvas.toDataURL('image/png');
492
509
  const text = await decodeOne(reader, url);
493
510
  const num = strictTracking(text);
494
- if (num) return num;
511
+ if (num) {
512
+ const r = barcodeRank(num);
513
+ if (r < bestRank) { best = num; bestRank = r; }
514
+ if (r <= 1) return num; // UPS or USPS: unambiguous, fast path
515
+ }
495
516
  }
496
517
  }
497
518
  } finally { bmp.close(); }
498
- return null;
519
+ return best;
499
520
  }
500
521
  // Detect when the pasted image is a screenshot OF THE APP itself (its own
501
522
  // header/buttons/tab bar get OCR'd) — the label inside is too small to read
package/public/extract.js CHANGED
@@ -59,13 +59,14 @@
59
59
  // Strict UPS structure: 1Z + 6-char shipper number + 2-digit service + 8-digit serial.
60
60
  var UPS_STRICT = /^1Z[0-9A-Z]{6}[0-9]{2}[0-9]{8}$/;
61
61
  function repairUpsZ(ups) {
62
- // Tesseract often reads the "1Z" anchor as "12" (or "IZ"). Repair it
63
- // only when the rest has strict UPS structure AND the shipper segment
64
- // contains a letter — real UPS shipper numbers do, random digit runs
65
- // don't. Never invent numbers from thin air.
62
+ // Tesseract often reads the "1Z" anchor as "12" (or "IZ", or "17" —
63
+ // 7→Z confusion). Repair it only when the rest has strict UPS structure
64
+ // AND the shipper segment contains a letter — real UPS shipper numbers
65
+ // do, random digit runs don't. Never invent numbers from thin air.
66
66
  var cand = null;
67
67
  if (/^12[0-9A-Z]{16}$/.test(ups)) cand = '1Z' + ups.slice(2);
68
68
  else if (/^IZ[0-9A-Z]{16}$/i.test(ups)) cand = '1Z' + ups.slice(2);
69
+ else if (/^17[0-9A-Z]{16}$/.test(ups)) cand = '1Z' + ups.slice(2);
69
70
  if (!cand || !UPS_STRICT.test(cand)) return null;
70
71
  if (!/[A-Z]/.test(cand.slice(2, 8))) return null;
71
72
  return cand;
@@ -73,7 +74,8 @@
73
74
 
74
75
  // UPS number split by spaces/dashes: "1Z 9999 99 ..." (or "12 ..."/"IZ ..." misreads)
75
76
  function spacedUpsIn(text, found, seen, conf) {
76
- var m, re = /\b(?:1Z|12|IZ)[\s-]?(?:[0-9A-Z][\s-]?){16}\b/gi;
77
+ // Anchor covers common 1Z misreads: 12, IZ, 17 (7→Z confusion)
78
+ var m, re = /\b(?:1Z|12|IZ|17)[\s-]?(?:[0-9A-Z][\s-]?){16}\b/gi;
77
79
  while ((m = re.exec(text)) !== null) {
78
80
  var ups = normalize(m[0]);
79
81
  var num = /^1Z[0-9A-Z]{16}$/.test(ups) ? ups : repairUpsZ(ups);
package/src/extract.js CHANGED
@@ -267,7 +267,8 @@
267
267
  runPatterns(z.text, found, seen, z.carrier);
268
268
  spacedUpsIn(z.text, found, seen, 'high');
269
269
  looseUpsIn(z.text, found, seen, 'high');
270
- spacedDigitGroupsIn(z.text, found, seen, 'high');
270
+ spacedDigitGroupsIn(z.text, found, seen, 'high', z.carrier);
271
+ if (z.carrier === 'ups') upsAnchorlessRecovery(z.text, found, seen);
271
272
  });
272
273
  // 1) Whole-text strict patterns
273
274
  runPatterns(text, found, seen, false);
@@ -305,7 +306,10 @@
305
306
  // anchor line + the two lines under it (the number is printed below)
306
307
  var slice = lines.slice(i, Math.min(lines.length, i + 3));
307
308
  var carrier = null;
308
- slice.forEach(function (sl) { carrier = carrier || anchorCarrier(sl); });
309
+ // Scan the slice AND the line above it: "UPS GROUND" typically sits
310
+ // directly above the "TRACKING #:" line, and the zone needs to know.
311
+ var scan = slice.concat(i > 0 ? [lines[i - 1]] : []);
312
+ scan.forEach(function (sl) { carrier = carrier || anchorCarrier(sl); });
309
313
  zones.push({ text: slice.join('\n'), carrier: carrier });
310
314
  }
311
315
  return zones;
@@ -377,7 +381,11 @@
377
381
  // contains a separator (contiguous runs are already covered by PATTERNS).
378
382
  // Joined candidates must validate against a carrier format; IMpb barcodes
379
383
  // yield their tracking number, never the routing.
380
- function spacedDigitGroupsIn(text, found, seen, conf) {
384
+ // zoneCarrier: when the anchor zone names a carrier ("UPS GROUND"), a
385
+ // candidate of any OTHER carrier is a fragment of the real number
386
+ // ("03 3985 0416" off a UPS label is not a DHL shipment) — demote it to
387
+ // low so it can only ever surface for review, never auto-add.
388
+ function spacedDigitGroupsIn(text, found, seen, conf, zoneCarrier) {
381
389
  var re = /\b\d(?:[\s-]?\d){9,33}\b/g, m;
382
390
  while ((m = re.exec(text)) !== null) {
383
391
  if (!/[\s-]/.test(m[0])) continue;
@@ -387,13 +395,34 @@
387
395
  var impb = parseImpb(num);
388
396
  if (impb) {
389
397
  seen[num] = true;
390
- if (!seen[impb]) { seen[impb] = true; found.push({ number: impb, carrier: 'usps', confidence: conf }); }
398
+ var ic = (zoneCarrier && zoneCarrier !== 'usps') ? 'low' : conf;
399
+ if (!seen[impb]) { seen[impb] = true; found.push({ number: impb, carrier: 'usps', confidence: ic }); }
391
400
  continue;
392
401
  }
393
402
  var carrier = guessCarrier(num);
394
403
  if (carrier === 'unknown') { seen[num] = true; continue; }
395
404
  seen[num] = true;
396
- found.push({ number: num, carrier: carrier, confidence: conf });
405
+ var c = (zoneCarrier && carrier !== zoneCarrier) ? 'low' : conf;
406
+ found.push({ number: num, carrier: carrier, confidence: c });
407
+ }
408
+ }
409
+
410
+ // Last resort for UPS zones: the "1Z" anchor itself was unreadable, so the
411
+ // number survives only as a 16-char alphanumeric tail ("X01 48H 03 3985
412
+ // 0416"). If it validates as strict UPS tail structure, offer 1Z+tail at
413
+ // medium (review-only, never auto-add) — far better than a DHL fragment.
414
+ var UPS_TAIL_STRICT = /^[0-9A-Z]{6}[0-9]{2}[0-9]{8}$/;
415
+ function upsAnchorlessRecovery(text, found, seen) {
416
+ var re = /\b([0-9A-Z](?:[\s-]?[0-9A-Z]){15})\b/gi, m;
417
+ while ((m = re.exec(text)) !== null) {
418
+ var token = normalize(m[1]);
419
+ if (/^(1Z|12|IZ|17)/i.test(token)) continue; // anchored — handled elsewhere
420
+ if (!UPS_TAIL_STRICT.test(token)) continue;
421
+ if (!/[A-Z]/.test(token.slice(0, 6))) continue; // real shipper codes have a letter
422
+ var num = '1Z' + token;
423
+ if (seen[num]) continue;
424
+ seen[num] = true;
425
+ found.push({ number: num, carrier: 'ups', confidence: 'medium', ocr_fixed: true });
397
426
  }
398
427
  }
399
428