hh-shipments 0.3.25 → 0.3.26

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/extract.js CHANGED
@@ -10,7 +10,8 @@
10
10
  // UPS: 1Z + 16 alphanumerics
11
11
  { carrier: 'ups', confidence: 'high', re: /\b(1Z[0-9A-Z]{16})\b/gi },
12
12
  // USPS: 20-22 digits starting 92-95 (9400..., 9205..., etc.)
13
- { carrier: 'usps', confidence: 'high', re: /\b(9[2-5]\d{18,20})\b/g },
13
+ // USPS: 20-22 digits starting 91-95 (9400..., 9305..., 9205..., 9101..., etc.)
14
+ { carrier: 'usps', confidence: 'high', re: /\b(9[1-5]\d{18,20})\b/g },
14
15
  // USPS S10 international: 2 letters + 9 digits + 2 letters (ends US = USPS)
15
16
  { carrier: 'usps', confidence: 'medium', re: /\b([A-Z]{2}\d{9}US)\b/g },
16
17
  // FedEx SmartPost / Ground 22-digit starting 96
@@ -247,9 +248,14 @@
247
248
  runPatterns(z.text, found, seen, z.carrier);
248
249
  spacedUpsIn(z.text, found, seen, 'high');
249
250
  looseUpsIn(z.text, found, seen, 'high');
251
+ spacedDigitGroupsIn(z.text, found, seen, 'high');
250
252
  });
251
253
  // 1) Whole-text strict patterns
252
254
  runPatterns(text, found, seen, false);
255
+ // 1b) Spaced digit groups: labels print "9305 5110 4390 0011 2062 42"
256
+ // with spaces, which strict PATTERNS cannot match. Without this pass a
257
+ // perfectly-read number is silently dropped.
258
+ spacedDigitGroupsIn(text, found, seen, 'medium');
253
259
  // 2) OCR-tolerant recovery pass
254
260
  extractOcrTolerant(text, found, seen);
255
261
  var rank = { high: 0, medium: 1, low: 2 };
@@ -294,6 +300,9 @@
294
300
  var num = normalize(m[1]);
295
301
  if (seen[num]) continue;
296
302
  seen[num] = true;
303
+ // Never accept an IMpb routing prefix (420 + 9 digits) as a FedEx
304
+ // number — it is postal routing, not a shipment.
305
+ if (p.carrier === 'fedex' && /^420\d{9}$/.test(num)) continue;
297
306
  // Context boost: carrier word near the number, or a hot-zone anchor
298
307
  // naming this carrier (a label's "UPS GROUND" line means the number
299
308
  // under it really is UPS).
@@ -314,12 +323,61 @@
314
323
  function isValidFormat(number, carrier) {
315
324
  var n = normalize(number);
316
325
  if (carrier === 'ups') return /^1Z[0-9A-Z]{16}$/.test(n);
317
- if (carrier === 'usps') return /^9[2-5]\d{18,20}$/.test(n) || /^[A-Z]{2}\d{9}US$/.test(n);
318
- if (carrier === 'fedex') return /^(96\d{20}|\d{12}|\d{15}|\d{20})$/.test(n);
326
+ if (carrier === 'usps') return /^9[1-5]\d{18,20}$/.test(n) || /^[A-Z]{2}\d{9}US$/.test(n);
327
+ // FedEx: never accept an IMpb routing prefix (420 + 9 digits) — it is
328
+ // postal routing, not a shipment number.
329
+ if (carrier === 'fedex') return /^(96\d{20}|\d{12}|\d{15}|\d{20})$/.test(n) && !/^420\d{9}$/.test(n);
319
330
  if (carrier === 'dhl') return /^\d{10,11}$/.test(n) || /^[A-Z]{2,4}\d{10,20}$/.test(n);
320
331
  return false;
321
332
  }
322
333
 
334
+ // Intelligent Mail package barcode: 420 + routing digits + USPS tracking,
335
+ // e.g. 420078742956 + 9305511043900011206242 (34 digits). The full barcode
336
+ // is NOT the tracking number — return the trailing tracking number. The
337
+ // routing portion must never pass as a shipment number.
338
+ function parseImpb(s) {
339
+ var num = normalize(s);
340
+ if (!/^420\d{23,31}$/.test(num)) return null; // 26–34 digits total
341
+ var m = /(9[1-5]\d{18,20})$/.exec(num);
342
+ return m ? m[1] : null;
343
+ }
344
+
345
+ // A spaced/dashed digit run is only plausible as a tracking number when
346
+ // every group has 2+ digits — this rejects ZIP+4 glued to a nearby lone
347
+ // digit ("07874-2956 1 lb"), phone fragments ("1-800-555-1234"), etc.
348
+ function validSpacedGroups(raw) {
349
+ var groups = String(raw).split(/[\s-]+/);
350
+ if (groups.length < 2) return true; // contiguous — decided elsewhere
351
+ for (var i = 0; i < groups.length; i++) if (groups[i].length < 2) return false;
352
+ return true;
353
+ }
354
+
355
+ // Digit groups split by spaces/dashes: "9305 5110 4390 0011 2062 42".
356
+ // Strict PATTERNS only match contiguous runs, so this pass exists for the
357
+ // standard label printing format. Only fires when the raw match actually
358
+ // contains a separator (contiguous runs are already covered by PATTERNS).
359
+ // Joined candidates must validate against a carrier format; IMpb barcodes
360
+ // yield their tracking number, never the routing.
361
+ function spacedDigitGroupsIn(text, found, seen, conf) {
362
+ var re = /\b\d(?:[\s-]?\d){9,33}\b/g, m;
363
+ while ((m = re.exec(text)) !== null) {
364
+ if (!/[\s-]/.test(m[0])) continue;
365
+ if (!validSpacedGroups(m[0])) continue;
366
+ var num = normalize(m[0]);
367
+ if (!num || seen[num]) continue;
368
+ var impb = parseImpb(num);
369
+ if (impb) {
370
+ seen[num] = true;
371
+ if (!seen[impb]) { seen[impb] = true; found.push({ number: impb, carrier: 'usps', confidence: conf }); }
372
+ continue;
373
+ }
374
+ var carrier = guessCarrier(num);
375
+ if (carrier === 'unknown') { seen[num] = true; continue; }
376
+ seen[num] = true;
377
+ found.push({ number: num, carrier: carrier, confidence: conf });
378
+ }
379
+ }
380
+
323
381
  function guessCarrier(num) {
324
382
  var carriers = ['ups', 'usps', 'fedex', 'dhl'];
325
383
  for (var i = 0; i < carriers.length; i++) if (isValidFormat(num, carriers[i])) return carriers[i];
@@ -349,13 +407,20 @@
349
407
  spacedUpsIn(text, found, seen, 'high');
350
408
  expandPunctHotZone(text, found, seen);
351
409
  looseUpsIn(text, found, seen, 'high');
352
- // b) long digit-like runs with OCR confusions -> fix -> re-match digit formats
353
- var runRe = /[0-9OILSBoilszb]{10,24}/g, run;
410
+ // b) long digit-like runs with OCR confusions -> fix -> re-match digit formats.
411
+ // Space-aware: OCR often reads "9305 51O0 ..." with both mangled chars
412
+ // AND spaces, so runs may contain separators. Runs up to 34 chars so a
413
+ // full 34-digit IMpb barcode is tested whole (parseImpb) instead of
414
+ // being split into chunks that false-positive as DHL/FedEx.
415
+ var runRe = /[0-9OILSBoilszb](?:[\s-]?[0-9OILSBoilszb]){9,33}/g, run;
354
416
  while ((run = runRe.exec(text)) !== null) {
417
+ if (/[\s-]/.test(run[0]) && !validSpacedGroups(run[0])) continue;
355
418
  var fixed = normalize(ocrDigitFix(run[0]));
356
- if (/^9[2-5]\d{18,20}$/.test(fixed)) push(fixed, 'usps', 'medium');
419
+ var impbFixed = parseImpb(fixed);
420
+ if (impbFixed) { push(impbFixed, 'usps', 'medium'); continue; }
421
+ if (/^9[1-5]\d{18,20}$/.test(fixed)) push(fixed, 'usps', 'medium');
357
422
  else if (/^96\d{20}$/.test(fixed)) push(fixed, 'fedex', 'medium');
358
- else if (/^(\d{12}|\d{15}|\d{20})$/.test(fixed)) push(fixed, 'fedex', 'medium');
423
+ else if (/^(\d{12}|\d{15}|\d{20})$/.test(fixed) && !/^420\d{9}$/.test(fixed)) push(fixed, 'fedex', 'medium');
359
424
  else if (/^\d{11}$/.test(fixed)) push(fixed, 'dhl', 'medium');
360
425
  else if (/^\d{10}$/.test(fixed)) push(fixed, 'dhl', 'low');
361
426
  }
@@ -369,6 +434,7 @@
369
434
  });
370
435
  tokens.sort(function (a, b) { return b.length - a.length; });
371
436
  tokens.slice(0, 3).forEach(function (t) {
437
+ if (/^420\d{9}$/.test(t)) return; // IMpb routing — never a shipment
372
438
  // UPS "1Z" misread as "12"/"IZ" in a joined (space-free) token
373
439
  var fixed = repairUpsZ(t);
374
440
  if (fixed) { push(fixed, 'ups', 'high'); return; }
@@ -393,7 +459,7 @@
393
459
  });
394
460
  }
395
461
 
396
- var api = { extractTrackingNumbers: extractTrackingNumbers, extractContext: extractContext, normalize: normalize, isValidFormat: isValidFormat };
462
+ var api = { extractTrackingNumbers: extractTrackingNumbers, extractContext: extractContext, normalize: normalize, isValidFormat: isValidFormat, parseImpb: parseImpb };
397
463
  if (typeof module !== 'undefined' && module.exports) module.exports = api;
398
464
  globalThis.HHExtract = api;
399
465
  })();