File size: 2,893 Bytes
ab37b00
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
67cca0d
 
 
 
ab37b00
67cca0d
 
 
 
 
 
 
 
ab37b00
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
import { db } from '../db/index.js';
import { envelopes } from '../db/schema.js';
import { sql } from 'drizzle-orm';

/** Cached normalized name → envelope_number map */
let _cache: Map<string, string> | null = null;

/** Normalize a name for fuzzy matching: lowercase, remove accents, trim whitespace */
function normalize(name: string): string {
  return name
    .normalize('NFD')
    .replace(/[\u0300-\u036f]/g, '')
    .toLowerCase()
    .replace(/\s+/g, ' ')
    .trim();
}

/** Build the cache from the envelopes table */
function ensureCache(): Map<string, string> {
  if (_cache) return _cache;

  const rows = db.select({
    envelopeNumber: envelopes.envelopeNumber,
    name: envelopes.name,
  }).from(envelopes).all();

  _cache = new Map();
  for (const row of rows) {
    if (!row.name || !row.envelopeNumber) continue;
    const key = normalize(row.name);
    // First match wins (lower envelope numbers take priority)
    if (!_cache.has(key)) {
      _cache.set(key, row.envelopeNumber);
    }
  }

  return _cache;
}

/**
 * Look up the envelope number for a given sender name.
 * Tries exact match first, then partial (last name) match.
 */
export function lookupEnvelopeNumber(senderName: string): string | null {
  if (!senderName) return null;

  const cache = ensureCache();
  const normalizedSender = normalize(senderName);

  // 1. Exact match
  const exact = cache.get(normalizedSender);
  if (exact) return exact;

  // 2. Word-level match: every word in the shorter name must appear as a
  //    whole word in the longer name.  This prevents "Anna" from matching
  //    "Giovanna" while still allowing "Jean Dupont" to match
  //    "Jean et Marie Dupont".
  for (const [envName, envNum] of cache) {
    const senderWords = normalizedSender.split(' ');
    const envWords = envName.split(' ');
    // Pick shorter set as the "query" words and the longer set as the pool
    const [query, pool] =
      senderWords.length <= envWords.length
        ? [senderWords, envWords]
        : [envWords, senderWords];
    if (query.length > 0 && query.every(w => pool.includes(w))) {
      return envNum;
    }
  }

  // 3. Last-name match: compare last words
  const senderParts = normalizedSender.split(' ');
  const senderLast = senderParts[senderParts.length - 1];
  if (senderLast.length >= 3) {
    for (const [envName, envNum] of cache) {
      const envParts = envName.split(' ');
      const envLast = envParts[envParts.length - 1];
      if (envLast === senderLast) {
        // Also check first name initial if available
        if (senderParts.length >= 2 && envParts.length >= 2) {
          if (envParts[0][0] === senderParts[0][0]) return envNum;
        } else {
          return envNum;
        }
      }
    }
  }

  return null;
}

/** Force cache rebuild (call after CSV re-import) */
export function clearEnvelopeCache(): void {
  _cache = null;
}