ezmedicationinput 0.1.0 → 0.1.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/dist/parser.js CHANGED
@@ -1,7 +1,13 @@
1
- import { DAY_OF_WEEK_TOKENS, DEFAULT_ROUTE_SYNONYMS, DEFAULT_UNIT_SYNONYMS, EVENT_TIMING_TOKENS, MEAL_KEYWORDS, ROUTE_TEXT, TIMING_ABBREVIATIONS, WORD_FREQUENCIES } from "./maps";
2
- import { inferUnitFromContext } from "./context";
3
- import { checkDiscouraged } from "./safety";
4
- import { EventTiming, FhirPeriodUnit, RouteCode } from "./types";
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.tokenize = tokenize;
4
+ exports.parseInternal = parseInternal;
5
+ const maps_1 = require("./maps");
6
+ const context_1 = require("./context");
7
+ const safety_1 = require("./safety");
8
+ const types_1 = require("./types");
9
+ const object_1 = require("./utils/object");
10
+ const array_1 = require("./utils/array");
5
11
  const BODY_SITE_HINTS = new Set([
6
12
  "left",
7
13
  "right",
@@ -31,75 +37,491 @@ const BODY_SITE_HINTS = new Set([
31
37
  "upper",
32
38
  "lower",
33
39
  "forearm",
34
- "back"
40
+ "back",
41
+ "mouth",
42
+ "tongue",
43
+ "tongues",
44
+ "cheek",
45
+ "cheeks",
46
+ "gum",
47
+ "gums",
48
+ "tooth",
49
+ "teeth",
50
+ "nose",
51
+ "nares",
52
+ "hair",
53
+ "skin",
54
+ "scalp",
55
+ "face",
56
+ "forehead",
57
+ "chin",
58
+ "neck",
59
+ "buttock",
60
+ "buttocks",
61
+ "gluteal",
62
+ "glute",
63
+ "muscle",
64
+ "muscles",
65
+ "vein",
66
+ "veins",
67
+ "vagina",
68
+ "vaginal",
69
+ "rectum",
70
+ "rectal",
71
+ "anus",
72
+ "perineum"
73
+ ]);
74
+ const SITE_CONNECTORS = new Set(["to", "in", "into", "on", "onto", "at"]);
75
+ const SITE_FILLER_WORDS = new Set([
76
+ "the",
77
+ "a",
78
+ "an",
79
+ "your",
80
+ "his",
81
+ "her",
82
+ "their",
83
+ "my"
84
+ ]);
85
+ const OCULAR_DIRECTION_WORDS = new Set([
86
+ "left",
87
+ "right",
88
+ "both",
89
+ "either",
90
+ "each",
91
+ "bilateral"
92
+ ]);
93
+ const OCULAR_SITE_WORDS = new Set([
94
+ "eye",
95
+ "eyes",
96
+ "eyelid",
97
+ "eyelids",
98
+ "ocular",
99
+ "ophthalmic",
100
+ "oculus"
35
101
  ]);
36
102
  const COMBO_EVENT_TIMINGS = {
37
- "early morning": EventTiming["Early Morning"],
38
- "late morning": EventTiming["Late Morning"],
39
- "early afternoon": EventTiming["Early Afternoon"],
40
- "late afternoon": EventTiming["Late Afternoon"],
41
- "early evening": EventTiming["Early Evening"],
42
- "late evening": EventTiming["Late Evening"],
43
- "after sleep": EventTiming["After Sleep"],
44
- "upon waking": EventTiming.Wake
103
+ "early morning": types_1.EventTiming["Early Morning"],
104
+ "late morning": types_1.EventTiming["Late Morning"],
105
+ "early afternoon": types_1.EventTiming["Early Afternoon"],
106
+ "late afternoon": types_1.EventTiming["Late Afternoon"],
107
+ "early evening": types_1.EventTiming["Early Evening"],
108
+ "late evening": types_1.EventTiming["Late Evening"],
109
+ "after sleep": types_1.EventTiming["After Sleep"],
110
+ "upon waking": types_1.EventTiming.Wake
45
111
  };
46
112
  // Tracking explicit breakfast/lunch/dinner markers lets the meal-expansion
47
113
  // logic bail early when the clinician already specified precise events.
48
114
  const SPECIFIC_MEAL_TIMINGS = new Set([
49
- EventTiming["Before Breakfast"],
50
- EventTiming["Before Lunch"],
51
- EventTiming["Before Dinner"],
52
- EventTiming["After Breakfast"],
53
- EventTiming["After Lunch"],
54
- EventTiming["After Dinner"],
55
- EventTiming.Breakfast,
56
- EventTiming.Lunch,
57
- EventTiming.Dinner
115
+ types_1.EventTiming["Before Breakfast"],
116
+ types_1.EventTiming["Before Lunch"],
117
+ types_1.EventTiming["Before Dinner"],
118
+ types_1.EventTiming["After Breakfast"],
119
+ types_1.EventTiming["After Lunch"],
120
+ types_1.EventTiming["After Dinner"],
121
+ types_1.EventTiming.Breakfast,
122
+ types_1.EventTiming.Lunch,
123
+ types_1.EventTiming.Dinner
58
124
  ]);
59
125
  // Ocular shorthand tokens commonly used in ophthalmic sigs.
60
126
  const EYE_SITE_TOKENS = {
61
- od: { site: "right eye", route: RouteCode["Ophthalmic route"] },
62
- re: { site: "right eye", route: RouteCode["Ophthalmic route"] },
63
- os: { site: "left eye", route: RouteCode["Ophthalmic route"] },
64
- le: { site: "left eye", route: RouteCode["Ophthalmic route"] },
65
- ou: { site: "both eyes", route: RouteCode["Ophthalmic route"] },
66
- be: { site: "both eyes", route: RouteCode["Ophthalmic route"] },
127
+ od: { site: "right eye", route: types_1.RouteCode["Ophthalmic route"] },
128
+ re: { site: "right eye", route: types_1.RouteCode["Ophthalmic route"] },
129
+ os: { site: "left eye", route: types_1.RouteCode["Ophthalmic route"] },
130
+ le: { site: "left eye", route: types_1.RouteCode["Ophthalmic route"] },
131
+ ou: { site: "both eyes", route: types_1.RouteCode["Ophthalmic route"] },
132
+ be: { site: "both eyes", route: types_1.RouteCode["Ophthalmic route"] },
67
133
  vod: {
68
134
  site: "right eye",
69
- route: RouteCode["Intravitreal route (qualifier value)"]
135
+ route: types_1.RouteCode["Intravitreal route (qualifier value)"]
70
136
  },
71
137
  vos: {
72
138
  site: "left eye",
73
- route: RouteCode["Intravitreal route (qualifier value)"]
139
+ route: types_1.RouteCode["Intravitreal route (qualifier value)"]
74
140
  },
75
141
  ivtod: {
76
142
  site: "right eye",
77
- route: RouteCode["Intravitreal route (qualifier value)"]
143
+ route: types_1.RouteCode["Intravitreal route (qualifier value)"]
78
144
  },
79
145
  ivtre: {
80
146
  site: "right eye",
81
- route: RouteCode["Intravitreal route (qualifier value)"]
147
+ route: types_1.RouteCode["Intravitreal route (qualifier value)"]
82
148
  },
83
149
  ivtos: {
84
150
  site: "left eye",
85
- route: RouteCode["Intravitreal route (qualifier value)"]
151
+ route: types_1.RouteCode["Intravitreal route (qualifier value)"]
86
152
  },
87
153
  ivtle: {
88
154
  site: "left eye",
89
- route: RouteCode["Intravitreal route (qualifier value)"]
155
+ route: types_1.RouteCode["Intravitreal route (qualifier value)"]
90
156
  },
91
157
  ivtou: {
92
158
  site: "both eyes",
93
- route: RouteCode["Intravitreal route (qualifier value)"]
159
+ route: types_1.RouteCode["Intravitreal route (qualifier value)"]
94
160
  },
95
161
  ivtbe: {
96
162
  site: "both eyes",
97
- route: RouteCode["Intravitreal route (qualifier value)"]
163
+ route: types_1.RouteCode["Intravitreal route (qualifier value)"]
98
164
  }
99
165
  };
100
- export function tokenize(input) {
166
+ const OPHTHALMIC_ROUTE_CODES = new Set([
167
+ types_1.RouteCode["Ophthalmic route"],
168
+ types_1.RouteCode["Intravitreal route (qualifier value)"]
169
+ ]);
170
+ const OPHTHALMIC_CONTEXT_TOKENS = new Set([
171
+ "drop",
172
+ "drops",
173
+ "gtt",
174
+ "gtts",
175
+ "eye",
176
+ "eyes",
177
+ "eyelid",
178
+ "eyelids",
179
+ "ocular",
180
+ "ophthalmic",
181
+ "ophth",
182
+ "oculus",
183
+ "os",
184
+ "ou",
185
+ "re",
186
+ "le",
187
+ "be"
188
+ ]);
189
+ function normalizeTokenLower(token) {
190
+ return token.lower.replace(/\./g, "");
191
+ }
192
+ function hasOphthalmicContextHint(tokens, index) {
193
+ for (let offset = -3; offset <= 3; offset++) {
194
+ if (offset === 0) {
195
+ continue;
196
+ }
197
+ const neighbor = tokens[index + offset];
198
+ if (!neighbor) {
199
+ continue;
200
+ }
201
+ const normalized = normalizeTokenLower(neighbor);
202
+ if (OPHTHALMIC_CONTEXT_TOKENS.has(normalized) || normalized.includes("eye")) {
203
+ return true;
204
+ }
205
+ }
206
+ return false;
207
+ }
208
+ function shouldInterpretOdAsOnceDaily(internal, tokens, index, treatAsSite) {
209
+ var _a;
210
+ if (treatAsSite) {
211
+ return false;
212
+ }
213
+ const hasCadenceAssigned = internal.frequency !== undefined ||
214
+ internal.frequencyMax !== undefined ||
215
+ internal.period !== undefined ||
216
+ internal.periodMax !== undefined ||
217
+ internal.timingCode !== undefined;
218
+ const hasPriorSiteContext = hasBodySiteContextBefore(internal, tokens, index);
219
+ const hasUpcomingSiteContext = hasBodySiteContextAfter(internal, tokens, index);
220
+ const previous = tokens[index - 1];
221
+ const previousNormalized = previous ? normalizeTokenLower(previous) : undefined;
222
+ const previousIsOd = previousNormalized === "od";
223
+ const previousConsumed = previousIsOd && internal.consumed.has(previous.index);
224
+ const previousOdProvidedSite = previousConsumed && /eye/i.test((_a = internal.siteText) !== null && _a !== void 0 ? _a : "");
225
+ if (previousOdProvidedSite) {
226
+ return true;
227
+ }
228
+ const previousEyeToken = previousNormalized && previousNormalized !== "od"
229
+ ? EYE_SITE_TOKENS[previousNormalized]
230
+ : undefined;
231
+ if (previousEyeToken && internal.consumed.has(previous.index)) {
232
+ return true;
233
+ }
234
+ if (previousNormalized === "od" &&
235
+ internal.siteSource === "abbreviation" &&
236
+ internal.siteText &&
237
+ /eye/i.test(internal.siteText)) {
238
+ return true;
239
+ }
240
+ if (hasPriorSiteContext || hasUpcomingSiteContext) {
241
+ return !hasCadenceAssigned;
242
+ }
243
+ if (hasCadenceAssigned) {
244
+ return false;
245
+ }
246
+ if (internal.routeCode && !OPHTHALMIC_ROUTE_CODES.has(internal.routeCode)) {
247
+ return true;
248
+ }
249
+ if (internal.unit && internal.unit !== "drop") {
250
+ return true;
251
+ }
252
+ if (internal.siteText && !/eye/i.test(internal.siteText)) {
253
+ return true;
254
+ }
255
+ const hasNonOdToken = tokens.some((token, tokenIndex) => {
256
+ if (tokenIndex === index) {
257
+ return false;
258
+ }
259
+ return normalizeTokenLower(token) !== "od";
260
+ });
261
+ if (!hasNonOdToken) {
262
+ return false;
263
+ }
264
+ const ophthalmicContext = hasOphthalmicContextHint(tokens, index) ||
265
+ (internal.routeCode !== undefined && OPHTHALMIC_ROUTE_CODES.has(internal.routeCode)) ||
266
+ (internal.siteText !== undefined && /eye/i.test(internal.siteText));
267
+ if (ophthalmicContext && hasSpelledOcularSiteBefore(tokens, index)) {
268
+ return true;
269
+ }
270
+ return !ophthalmicContext;
271
+ }
272
+ function hasBodySiteContextBefore(internal, tokens, index) {
273
+ const currentToken = tokens[index];
274
+ const currentTokenIndex = currentToken ? currentToken.index : index;
275
+ if (internal.siteText) {
276
+ return true;
277
+ }
278
+ for (const tokenIndex of internal.siteTokenIndices) {
279
+ if (tokenIndex < currentTokenIndex) {
280
+ return true;
281
+ }
282
+ }
283
+ for (let i = 0; i < index; i++) {
284
+ const token = tokens[i];
285
+ if (!token) {
286
+ continue;
287
+ }
288
+ if (internal.consumed.has(token.index)) {
289
+ if (internal.siteTokenIndices.has(token.index) && token.index < currentTokenIndex) {
290
+ return true;
291
+ }
292
+ continue;
293
+ }
294
+ const normalized = normalizeTokenLower(token);
295
+ if (BODY_SITE_HINTS.has(normalized)) {
296
+ return true;
297
+ }
298
+ if (EYE_SITE_TOKENS[normalized]) {
299
+ return true;
300
+ }
301
+ }
302
+ return false;
303
+ }
304
+ function hasBodySiteContextAfter(internal, tokens, index) {
305
+ const currentToken = tokens[index];
306
+ const currentTokenIndex = currentToken ? currentToken.index : index;
307
+ for (const tokenIndex of internal.siteTokenIndices) {
308
+ if (tokenIndex > currentTokenIndex) {
309
+ return true;
310
+ }
311
+ }
312
+ let seenConnector = false;
313
+ for (let i = index + 1; i < tokens.length; i++) {
314
+ const token = tokens[i];
315
+ if (!token) {
316
+ continue;
317
+ }
318
+ if (internal.consumed.has(token.index)) {
319
+ if (internal.siteTokenIndices.has(token.index) && token.index > currentTokenIndex) {
320
+ return true;
321
+ }
322
+ continue;
323
+ }
324
+ const normalized = normalizeTokenLower(token);
325
+ if (SITE_CONNECTORS.has(normalized)) {
326
+ seenConnector = true;
327
+ continue;
328
+ }
329
+ if (SITE_FILLER_WORDS.has(normalized)) {
330
+ continue;
331
+ }
332
+ if (BODY_SITE_HINTS.has(normalized)) {
333
+ return true;
334
+ }
335
+ if (seenConnector) {
336
+ break;
337
+ }
338
+ if (!seenConnector) {
339
+ break;
340
+ }
341
+ }
342
+ return false;
343
+ }
344
+ function hasSpelledOcularSiteBefore(tokens, index) {
345
+ let hasOcularWord = false;
346
+ let hasDirectionalCue = false;
347
+ for (let i = 0; i < index; i++) {
348
+ const token = tokens[i];
349
+ if (!token) {
350
+ continue;
351
+ }
352
+ const normalized = normalizeTokenLower(token);
353
+ if (SITE_CONNECTORS.has(normalized) || OCULAR_DIRECTION_WORDS.has(normalized)) {
354
+ hasDirectionalCue = true;
355
+ }
356
+ if (OCULAR_SITE_WORDS.has(normalized) || normalized.includes("eye")) {
357
+ hasOcularWord = true;
358
+ }
359
+ if (hasDirectionalCue && hasOcularWord) {
360
+ return true;
361
+ }
362
+ }
363
+ return false;
364
+ }
365
+ function shouldTreatEyeTokenAsSite(internal, tokens, index, context) {
366
+ var _a;
367
+ const currentToken = tokens[index];
368
+ const normalizedSelf = normalizeTokenLower(currentToken);
369
+ const eyeMeta = EYE_SITE_TOKENS[normalizedSelf];
370
+ if (internal.routeCode && !OPHTHALMIC_ROUTE_CODES.has(internal.routeCode)) {
371
+ return false;
372
+ }
373
+ if (internal.siteText) {
374
+ return false;
375
+ }
376
+ if (internal.siteSource === "abbreviation") {
377
+ return false;
378
+ }
379
+ const dosageForm = (_a = context === null || context === void 0 ? void 0 : context.dosageForm) === null || _a === void 0 ? void 0 : _a.toLowerCase();
380
+ const contextImpliesOphthalmic = Boolean(dosageForm && /(eye|ophth|ocular|intravit)/i.test(dosageForm));
381
+ const eyeRouteImpliesOphthalmic = (eyeMeta === null || eyeMeta === void 0 ? void 0 : eyeMeta.route) === types_1.RouteCode["Intravitreal route (qualifier value)"];
382
+ const ophthalmicContext = hasOphthalmicContextHint(tokens, index) ||
383
+ (internal.routeCode !== undefined && OPHTHALMIC_ROUTE_CODES.has(internal.routeCode)) ||
384
+ contextImpliesOphthalmic ||
385
+ eyeRouteImpliesOphthalmic;
386
+ if (hasBodySiteContextAfter(internal, tokens, index)) {
387
+ return false;
388
+ }
389
+ if (!ophthalmicContext) {
390
+ const hasOtherActiveTokens = tokens.some((token, tokenIndex) => tokenIndex !== index && !internal.consumed.has(token.index));
391
+ const onlyEyeTokens = tokens.every((token, tokenIndex) => {
392
+ if (tokenIndex === index || internal.consumed.has(token.index)) {
393
+ return true;
394
+ }
395
+ return normalizeTokenLower(token) === "od";
396
+ });
397
+ if (!hasOtherActiveTokens) {
398
+ return internal.unit === undefined && internal.routeCode === undefined;
399
+ }
400
+ if (onlyEyeTokens) {
401
+ return true;
402
+ }
403
+ return false;
404
+ }
405
+ for (let i = 0; i < index; i++) {
406
+ const candidate = tokens[i];
407
+ if (internal.consumed.has(candidate.index)) {
408
+ continue;
409
+ }
410
+ const normalized = normalizeTokenLower(candidate);
411
+ if (SITE_CONNECTORS.has(normalized)) {
412
+ continue;
413
+ }
414
+ if (BODY_SITE_HINTS.has(normalized)) {
415
+ return false;
416
+ }
417
+ if (EYE_SITE_TOKENS[normalized]) {
418
+ return false;
419
+ }
420
+ if (maps_1.DEFAULT_ROUTE_SYNONYMS[normalized]) {
421
+ return false;
422
+ }
423
+ }
424
+ return true;
425
+ }
426
+ function tryParseNumericCadence(internal, tokens, index) {
427
+ const token = tokens[index];
428
+ if (!/^[0-9]+(?:\.[0-9]+)?$/.test(token.lower)) {
429
+ return false;
430
+ }
431
+ if (internal.frequency !== undefined ||
432
+ internal.frequencyMax !== undefined ||
433
+ internal.period !== undefined ||
434
+ internal.periodMax !== undefined) {
435
+ return false;
436
+ }
437
+ let nextIndex = index + 1;
438
+ const connectors = [];
439
+ while (true) {
440
+ const connector = tokens[nextIndex];
441
+ if (!connector || internal.consumed.has(connector.index)) {
442
+ break;
443
+ }
444
+ const normalized = normalizeTokenLower(connector);
445
+ if (normalized === "per" || normalized === "a" || normalized === "each" || normalized === "every") {
446
+ connectors.push(connector);
447
+ nextIndex += 1;
448
+ continue;
449
+ }
450
+ break;
451
+ }
452
+ if (!connectors.length) {
453
+ return false;
454
+ }
455
+ const unitToken = tokens[nextIndex];
456
+ if (!unitToken || internal.consumed.has(unitToken.index)) {
457
+ return false;
458
+ }
459
+ const unitCode = mapIntervalUnit(normalizeTokenLower(unitToken));
460
+ if (!unitCode) {
461
+ return false;
462
+ }
463
+ const value = parseFloat(token.original);
464
+ if (!Number.isFinite(value)) {
465
+ return false;
466
+ }
467
+ internal.frequency = value;
468
+ internal.period = 1;
469
+ internal.periodUnit = unitCode;
470
+ if (value === 1 && unitCode === types_1.FhirPeriodUnit.Day && !internal.timingCode) {
471
+ internal.timingCode = "QD";
472
+ }
473
+ mark(internal.consumed, token);
474
+ for (const connector of connectors) {
475
+ mark(internal.consumed, connector);
476
+ }
477
+ mark(internal.consumed, unitToken);
478
+ return true;
479
+ }
480
+ const SITE_UNIT_ROUTE_HINTS = [
481
+ { pattern: /\beye(s)?\b/i, route: types_1.RouteCode["Ophthalmic route"] },
482
+ { pattern: /\beyelid(s)?\b/i, route: types_1.RouteCode["Ophthalmic route"] },
483
+ { pattern: /\bintravitreal\b/i, route: types_1.RouteCode["Intravitreal route (qualifier value)"] },
484
+ { pattern: /\bear(s)?\b/i, route: types_1.RouteCode["Otic route"] },
485
+ { pattern: /\bnostril(s)?\b/i, route: types_1.RouteCode["Nasal route"] },
486
+ { pattern: /\bnares?\b/i, route: types_1.RouteCode["Nasal route"] },
487
+ { pattern: /\bnose\b/i, route: types_1.RouteCode["Nasal route"] },
488
+ { pattern: /\bmouth\b/i, route: types_1.RouteCode["Oral route"] },
489
+ { pattern: /\boral\b/i, route: types_1.RouteCode["Oral route"] },
490
+ { pattern: /\bunder (the )?tongue\b/i, route: types_1.RouteCode["Sublingual route"] },
491
+ { pattern: /\btongue\b/i, route: types_1.RouteCode["Sublingual route"] },
492
+ { pattern: /\bcheek(s)?\b/i, route: types_1.RouteCode["Buccal route"] },
493
+ { pattern: /\blung(s)?\b/i, route: types_1.RouteCode["Respiratory tract route (qualifier value)"] },
494
+ { pattern: /\brespiratory tract\b/i, route: types_1.RouteCode["Respiratory tract route (qualifier value)"] },
495
+ { pattern: /\bskin\b/i, route: types_1.RouteCode["Topical route"] },
496
+ { pattern: /\bscalp\b/i, route: types_1.RouteCode["Topical route"] },
497
+ { pattern: /\bface\b/i, route: types_1.RouteCode["Topical route"] },
498
+ { pattern: /\bhand(s)?\b/i, route: types_1.RouteCode["Topical route"] },
499
+ { pattern: /(\bfoot\b|\bfeet\b)/i, route: types_1.RouteCode["Topical route"] },
500
+ { pattern: /\belbow(s)?\b/i, route: types_1.RouteCode["Topical route"] },
501
+ { pattern: /\bknee(s)?\b/i, route: types_1.RouteCode["Topical route"] },
502
+ { pattern: /\bleg(s)?\b/i, route: types_1.RouteCode["Topical route"] },
503
+ { pattern: /\barm(s)?\b/i, route: types_1.RouteCode["Topical route"] },
504
+ { pattern: /\bpatch(es)?\b/i, route: types_1.RouteCode["Transdermal route"] },
505
+ { pattern: /\babdomen\b/i, route: types_1.RouteCode["Subcutaneous route"] },
506
+ { pattern: /\bbelly\b/i, route: types_1.RouteCode["Subcutaneous route"] },
507
+ { pattern: /\bstomach\b/i, route: types_1.RouteCode["Subcutaneous route"] },
508
+ { pattern: /\bthigh(s)?\b/i, route: types_1.RouteCode["Subcutaneous route"] },
509
+ { pattern: /\bupper arm\b/i, route: types_1.RouteCode["Subcutaneous route"] },
510
+ { pattern: /\bbuttock(s)?\b/i, route: types_1.RouteCode["Intramuscular route"] },
511
+ { pattern: /\bglute(al)?\b/i, route: types_1.RouteCode["Intramuscular route"] },
512
+ { pattern: /\bdeltoid\b/i, route: types_1.RouteCode["Intramuscular route"] },
513
+ { pattern: /\bmuscle(s)?\b/i, route: types_1.RouteCode["Intramuscular route"] },
514
+ { pattern: /\bvein(s)?\b/i, route: types_1.RouteCode["Intravenous route"] },
515
+ { pattern: /\brectum\b/i, route: types_1.RouteCode["Per rectum"] },
516
+ { pattern: /\banus\b/i, route: types_1.RouteCode["Per rectum"] },
517
+ { pattern: /\brectal\b/i, route: types_1.RouteCode["Per rectum"] },
518
+ { pattern: /\bvagina\b/i, route: types_1.RouteCode["Per vagina"] },
519
+ { pattern: /\bvaginal\b/i, route: types_1.RouteCode["Per vagina"] }
520
+ ];
521
+ function tokenize(input) {
101
522
  const separators = /[(),]/g;
102
523
  let normalized = input.trim().replace(separators, " ");
524
+ normalized = normalized.replace(/(\d+(?:\.\d+)?)\s*\/\s*(d|day|days|wk|w|week|weeks|mo|month|months|hr|hrs|hour|hours|h|min|mins|minute|minutes)\b/gi, (_match, value, unit) => `${value} per ${unit}`);
103
525
  normalized = normalized.replace(/(\d+)\s*\/\s*(\d+)/g, (match, num, den) => {
104
526
  const numerator = parseFloat(num);
105
527
  const denominator = parseFloat(den);
@@ -154,7 +576,7 @@ function mark(consumed, token) {
154
576
  consumed.add(token.index);
155
577
  }
156
578
  function addWhen(target, code) {
157
- if (!target.includes(code)) {
579
+ if (!(0, array_1.arrayIncludes)(target, code)) {
158
580
  target.push(code);
159
581
  }
160
582
  }
@@ -173,68 +595,69 @@ function computeMealExpansions(base, frequency, pairPreference) {
173
595
  if (frequency < 1 || frequency > 4) {
174
596
  return undefined;
175
597
  }
176
- const bedtime = EventTiming["Before Sleep"];
598
+ const bedtime = types_1.EventTiming["Before Sleep"];
177
599
  const beforePair = pairPreference === "breakfast+lunch"
178
- ? [EventTiming["Before Breakfast"], EventTiming["Before Lunch"]]
179
- : [EventTiming["Before Breakfast"], EventTiming["Before Dinner"]];
600
+ ? [types_1.EventTiming["Before Breakfast"], types_1.EventTiming["Before Lunch"]]
601
+ : [types_1.EventTiming["Before Breakfast"], types_1.EventTiming["Before Dinner"]];
180
602
  const afterPair = pairPreference === "breakfast+lunch"
181
- ? [EventTiming["After Breakfast"], EventTiming["After Lunch"]]
182
- : [EventTiming["After Breakfast"], EventTiming["After Dinner"]];
603
+ ? [types_1.EventTiming["After Breakfast"], types_1.EventTiming["After Lunch"]]
604
+ : [types_1.EventTiming["After Breakfast"], types_1.EventTiming["After Dinner"]];
183
605
  const withPair = pairPreference === "breakfast+lunch"
184
- ? [EventTiming.Breakfast, EventTiming.Lunch]
185
- : [EventTiming.Breakfast, EventTiming.Dinner];
606
+ ? [types_1.EventTiming.Breakfast, types_1.EventTiming.Lunch]
607
+ : [types_1.EventTiming.Breakfast, types_1.EventTiming.Dinner];
186
608
  if (base === "before") {
187
609
  if (frequency === 1)
188
- return [EventTiming["Before Breakfast"]];
610
+ return [types_1.EventTiming["Before Breakfast"]];
189
611
  if (frequency === 2)
190
612
  return beforePair;
191
613
  if (frequency === 3) {
192
614
  return [
193
- EventTiming["Before Breakfast"],
194
- EventTiming["Before Lunch"],
195
- EventTiming["Before Dinner"]
615
+ types_1.EventTiming["Before Breakfast"],
616
+ types_1.EventTiming["Before Lunch"],
617
+ types_1.EventTiming["Before Dinner"]
196
618
  ];
197
619
  }
198
620
  return [
199
- EventTiming["Before Breakfast"],
200
- EventTiming["Before Lunch"],
201
- EventTiming["Before Dinner"],
621
+ types_1.EventTiming["Before Breakfast"],
622
+ types_1.EventTiming["Before Lunch"],
623
+ types_1.EventTiming["Before Dinner"],
202
624
  bedtime
203
625
  ];
204
626
  }
205
627
  if (base === "after") {
206
628
  if (frequency === 1)
207
- return [EventTiming["After Breakfast"]];
629
+ return [types_1.EventTiming["After Breakfast"]];
208
630
  if (frequency === 2)
209
631
  return afterPair;
210
632
  if (frequency === 3) {
211
633
  return [
212
- EventTiming["After Breakfast"],
213
- EventTiming["After Lunch"],
214
- EventTiming["After Dinner"]
634
+ types_1.EventTiming["After Breakfast"],
635
+ types_1.EventTiming["After Lunch"],
636
+ types_1.EventTiming["After Dinner"]
215
637
  ];
216
638
  }
217
639
  return [
218
- EventTiming["After Breakfast"],
219
- EventTiming["After Lunch"],
220
- EventTiming["After Dinner"],
640
+ types_1.EventTiming["After Breakfast"],
641
+ types_1.EventTiming["After Lunch"],
642
+ types_1.EventTiming["After Dinner"],
221
643
  bedtime
222
644
  ];
223
645
  }
224
646
  // base === "with"
225
647
  if (frequency === 1)
226
- return [EventTiming.Breakfast];
648
+ return [types_1.EventTiming.Breakfast];
227
649
  if (frequency === 2)
228
650
  return withPair;
229
651
  if (frequency === 3) {
230
- return [EventTiming.Breakfast, EventTiming.Lunch, EventTiming.Dinner];
652
+ return [types_1.EventTiming.Breakfast, types_1.EventTiming.Lunch, types_1.EventTiming.Dinner];
231
653
  }
232
- return [EventTiming.Breakfast, EventTiming.Lunch, EventTiming.Dinner, bedtime];
654
+ return [types_1.EventTiming.Breakfast, types_1.EventTiming.Lunch, types_1.EventTiming.Dinner, bedtime];
233
655
  }
234
656
  // Optionally replace generic meal tokens with concrete breakfast/lunch/dinner
235
657
  // EventTiming codes when the cadence makes the intent obvious.
236
658
  function expandMealTimings(internal, options) {
237
- if (!options?.smartMealExpansion) {
659
+ var _a;
660
+ if (!(options === null || options === void 0 ? void 0 : options.smartMealExpansion)) {
238
661
  return;
239
662
  }
240
663
  if (!internal.when.length) {
@@ -249,7 +672,7 @@ function expandMealTimings(internal, options) {
249
672
  }
250
673
  if (internal.period !== undefined &&
251
674
  internal.periodUnit !== undefined &&
252
- (internal.periodUnit !== FhirPeriodUnit.Day || internal.period !== 1)) {
675
+ (internal.periodUnit !== types_1.FhirPeriodUnit.Day || internal.period !== 1)) {
253
676
  return;
254
677
  }
255
678
  if (internal.period !== undefined &&
@@ -257,30 +680,30 @@ function expandMealTimings(internal, options) {
257
680
  internal.period !== 1) {
258
681
  return;
259
682
  }
260
- if (internal.periodUnit && internal.periodUnit !== FhirPeriodUnit.Day) {
683
+ if (internal.periodUnit && internal.periodUnit !== types_1.FhirPeriodUnit.Day) {
261
684
  return;
262
685
  }
263
686
  if (internal.frequencyMax !== undefined || internal.periodMax !== undefined) {
264
687
  return;
265
688
  }
266
- const pairPreference = options.twoPerDayPair ?? "breakfast+dinner";
689
+ const pairPreference = (_a = options.twoPerDayPair) !== null && _a !== void 0 ? _a : "breakfast+dinner";
267
690
  const replacements = [];
268
- if (internal.when.includes(EventTiming["Before Meal"])) {
691
+ if ((0, array_1.arrayIncludes)(internal.when, types_1.EventTiming["Before Meal"])) {
269
692
  const specifics = computeMealExpansions("before", frequency, pairPreference);
270
693
  if (specifics) {
271
- replacements.push({ general: EventTiming["Before Meal"], specifics });
694
+ replacements.push({ general: types_1.EventTiming["Before Meal"], specifics });
272
695
  }
273
696
  }
274
- if (internal.when.includes(EventTiming["After Meal"])) {
697
+ if ((0, array_1.arrayIncludes)(internal.when, types_1.EventTiming["After Meal"])) {
275
698
  const specifics = computeMealExpansions("after", frequency, pairPreference);
276
699
  if (specifics) {
277
- replacements.push({ general: EventTiming["After Meal"], specifics });
700
+ replacements.push({ general: types_1.EventTiming["After Meal"], specifics });
278
701
  }
279
702
  }
280
- if (internal.when.includes(EventTiming.Meal)) {
703
+ if ((0, array_1.arrayIncludes)(internal.when, types_1.EventTiming.Meal)) {
281
704
  const specifics = computeMealExpansions("with", frequency, pairPreference);
282
705
  if (specifics) {
283
- replacements.push({ general: EventTiming.Meal, specifics });
706
+ replacements.push({ general: types_1.EventTiming.Meal, specifics });
284
707
  }
285
708
  }
286
709
  for (const { general, specifics } of replacements) {
@@ -292,15 +715,15 @@ function expandMealTimings(internal, options) {
292
715
  }
293
716
  function setRoute(internal, code, text) {
294
717
  internal.routeCode = code;
295
- internal.routeText = text ?? ROUTE_TEXT[code];
718
+ internal.routeText = text !== null && text !== void 0 ? text : maps_1.ROUTE_TEXT[code];
296
719
  }
297
720
  /**
298
721
  * Convert hour-based values into minutes when fractional quantities appear so
299
722
  * the resulting FHIR repeat payloads avoid unwieldy decimals.
300
723
  */
301
724
  function normalizePeriodValue(value, unit) {
302
- if (unit === FhirPeriodUnit.Hour && (!Number.isInteger(value) || value < 1)) {
303
- return { value: Math.round(value * 60 * 1000) / 1000, unit: FhirPeriodUnit.Minute };
725
+ if (unit === types_1.FhirPeriodUnit.Hour && (!Number.isInteger(value) || value < 1)) {
726
+ return { value: Math.round(value * 60 * 1000) / 1000, unit: types_1.FhirPeriodUnit.Minute };
304
727
  }
305
728
  return { value, unit };
306
729
  }
@@ -309,28 +732,28 @@ function normalizePeriodValue(value, unit) {
309
732
  * demand conversion into minutes.
310
733
  */
311
734
  function normalizePeriodRange(low, high, unit) {
312
- if (unit === FhirPeriodUnit.Hour && (!Number.isInteger(low) || !Number.isInteger(high) || low < 1 || high < 1)) {
735
+ if (unit === types_1.FhirPeriodUnit.Hour && (!Number.isInteger(low) || !Number.isInteger(high) || low < 1 || high < 1)) {
313
736
  return {
314
737
  low: Math.round(low * 60 * 1000) / 1000,
315
738
  high: Math.round(high * 60 * 1000) / 1000,
316
- unit: FhirPeriodUnit.Minute
739
+ unit: types_1.FhirPeriodUnit.Minute
317
740
  };
318
741
  }
319
742
  return { low, high, unit };
320
743
  }
321
744
  function periodUnitSuffix(unit) {
322
745
  switch (unit) {
323
- case FhirPeriodUnit.Minute:
746
+ case types_1.FhirPeriodUnit.Minute:
324
747
  return "min";
325
- case FhirPeriodUnit.Hour:
748
+ case types_1.FhirPeriodUnit.Hour:
326
749
  return "h";
327
- case FhirPeriodUnit.Day:
750
+ case types_1.FhirPeriodUnit.Day:
328
751
  return "d";
329
- case FhirPeriodUnit.Week:
752
+ case types_1.FhirPeriodUnit.Week:
330
753
  return "wk";
331
- case FhirPeriodUnit.Month:
754
+ case types_1.FhirPeriodUnit.Month:
332
755
  return "mo";
333
- case FhirPeriodUnit.Year:
756
+ case types_1.FhirPeriodUnit.Year:
334
757
  return "a";
335
758
  default:
336
759
  return undefined;
@@ -342,8 +765,8 @@ function maybeAssignTimingCode(internal, value, unit) {
342
765
  return;
343
766
  }
344
767
  const key = `q${value}${suffix}`;
345
- const descriptor = TIMING_ABBREVIATIONS[key];
346
- if (descriptor?.code && !internal.timingCode) {
768
+ const descriptor = maps_1.TIMING_ABBREVIATIONS[key];
769
+ if ((descriptor === null || descriptor === void 0 ? void 0 : descriptor.code) && !internal.timingCode) {
347
770
  internal.timingCode = descriptor.code;
348
771
  }
349
772
  }
@@ -352,18 +775,19 @@ function maybeAssignTimingCode(internal, value, unit) {
352
775
  * period clearly represents common cadences (daily/weekly/monthly).
353
776
  */
354
777
  function applyPeriod(internal, period, unit) {
778
+ var _a, _b, _c;
355
779
  const normalized = normalizePeriodValue(period, unit);
356
780
  internal.period = normalized.value;
357
781
  internal.periodUnit = normalized.unit;
358
782
  maybeAssignTimingCode(internal, normalized.value, normalized.unit);
359
- if (normalized.unit === FhirPeriodUnit.Day && normalized.value === 1) {
360
- internal.frequency = internal.frequency ?? 1;
783
+ if (normalized.unit === types_1.FhirPeriodUnit.Day && normalized.value === 1) {
784
+ internal.frequency = (_a = internal.frequency) !== null && _a !== void 0 ? _a : 1;
361
785
  }
362
- if (normalized.unit === FhirPeriodUnit.Week && normalized.value === 1) {
363
- internal.timingCode = internal.timingCode ?? "WK";
786
+ if (normalized.unit === types_1.FhirPeriodUnit.Week && normalized.value === 1) {
787
+ internal.timingCode = (_b = internal.timingCode) !== null && _b !== void 0 ? _b : "WK";
364
788
  }
365
- if (normalized.unit === FhirPeriodUnit.Month && normalized.value === 1) {
366
- internal.timingCode = internal.timingCode ?? "MO";
789
+ if (normalized.unit === types_1.FhirPeriodUnit.Month && normalized.value === 1) {
790
+ internal.timingCode = (_c = internal.timingCode) !== null && _c !== void 0 ? _c : "MO";
367
791
  }
368
792
  }
369
793
  /**
@@ -406,7 +830,7 @@ function tryParseCompactQ(internal, tokens, index) {
406
830
  }
407
831
  function applyFrequencyDescriptor(internal, token, descriptor, options) {
408
832
  if (descriptor.discouraged) {
409
- const check = checkDiscouraged(token.original, options);
833
+ const check = (0, safety_1.checkDiscouraged)(token.original, options);
410
834
  if (check.warning) {
411
835
  internal.warnings.push(check.warning);
412
836
  }
@@ -447,11 +871,11 @@ function parseMealContext(internal, tokens, index, code) {
447
871
  applyWhenToken(internal, token, code);
448
872
  return;
449
873
  }
450
- const meal = MEAL_KEYWORDS[next.lower];
874
+ const meal = maps_1.MEAL_KEYWORDS[next.lower];
451
875
  if (meal) {
452
- const whenCode = code === EventTiming["After Meal"]
876
+ const whenCode = code === types_1.EventTiming["After Meal"]
453
877
  ? meal.pc
454
- : code === EventTiming["Before Meal"]
878
+ : code === types_1.EventTiming["Before Meal"]
455
879
  ? meal.ac
456
880
  : code;
457
881
  applyWhenToken(internal, token, whenCode);
@@ -519,19 +943,19 @@ function mapIntervalUnit(token) {
519
943
  token === "minute" ||
520
944
  token === "minutes" ||
521
945
  token === "m") {
522
- return FhirPeriodUnit.Minute;
946
+ return types_1.FhirPeriodUnit.Minute;
523
947
  }
524
948
  if (token === "h" || token === "hr" || token === "hrs" || token === "hour" || token === "hours") {
525
- return FhirPeriodUnit.Hour;
949
+ return types_1.FhirPeriodUnit.Hour;
526
950
  }
527
951
  if (token === "d" || token === "day" || token === "days") {
528
- return FhirPeriodUnit.Day;
952
+ return types_1.FhirPeriodUnit.Day;
529
953
  }
530
954
  if (token === "wk" || token === "w" || token === "week" || token === "weeks") {
531
- return FhirPeriodUnit.Week;
955
+ return types_1.FhirPeriodUnit.Week;
532
956
  }
533
957
  if (token === "mo" || token === "month" || token === "months") {
534
- return FhirPeriodUnit.Month;
958
+ return types_1.FhirPeriodUnit.Month;
535
959
  }
536
960
  return undefined;
537
961
  }
@@ -547,7 +971,8 @@ function parseNumericRange(token) {
547
971
  }
548
972
  return { low, high };
549
973
  }
550
- export function parseInternal(input, options) {
974
+ function parseInternal(input, options) {
975
+ var _a, _b, _c, _d, _e, _f;
551
976
  const tokens = tokenize(input);
552
977
  const internal = {
553
978
  input,
@@ -555,11 +980,12 @@ export function parseInternal(input, options) {
555
980
  consumed: new Set(),
556
981
  dayOfWeek: [],
557
982
  when: [],
558
- warnings: []
983
+ warnings: [],
984
+ siteTokenIndices: new Set()
559
985
  };
560
- const context = options?.context ?? undefined;
561
- const customRouteMap = options?.routeMap
562
- ? new Map(Object.entries(options.routeMap).map(([key, value]) => [
986
+ const context = (_a = options === null || options === void 0 ? void 0 : options.context) !== null && _a !== void 0 ? _a : undefined;
987
+ const customRouteMap = (options === null || options === void 0 ? void 0 : options.routeMap)
988
+ ? new Map((0, object_1.objectEntries)(options.routeMap).map(([key, value]) => [
563
989
  key.toLowerCase(),
564
990
  value
565
991
  ]))
@@ -577,12 +1003,12 @@ export function parseInternal(input, options) {
577
1003
  prnReasonStart = i + 1;
578
1004
  break;
579
1005
  }
580
- if (token.lower === "as" && tokens[i + 1]?.lower === "needed") {
1006
+ if (token.lower === "as" && ((_b = tokens[i + 1]) === null || _b === void 0 ? void 0 : _b.lower) === "needed") {
581
1007
  internal.asNeeded = true;
582
1008
  mark(internal.consumed, token);
583
1009
  mark(internal.consumed, tokens[i + 1]);
584
1010
  let reasonIndex = i + 2;
585
- if (tokens[reasonIndex]?.lower === "for") {
1011
+ if (((_c = tokens[reasonIndex]) === null || _c === void 0 ? void 0 : _c.lower) === "for") {
586
1012
  mark(internal.consumed, tokens[reasonIndex]);
587
1013
  reasonIndex += 1;
588
1014
  }
@@ -603,27 +1029,30 @@ export function parseInternal(input, options) {
603
1029
  }
604
1030
  internal.frequency = freq;
605
1031
  internal.period = 1;
606
- internal.periodUnit = FhirPeriodUnit.Day;
1032
+ internal.periodUnit = types_1.FhirPeriodUnit.Day;
607
1033
  mark(internal.consumed, token);
608
1034
  }
609
1035
  }
610
1036
  // Process tokens sequentially
611
1037
  const tryRouteSynonym = (startIndex) => {
612
- const maxSpan = Math.min(5, tokens.length - startIndex);
1038
+ const maxSpan = Math.min(24, tokens.length - startIndex);
613
1039
  for (let span = maxSpan; span >= 1; span--) {
614
1040
  const slice = tokens.slice(startIndex, startIndex + span);
615
1041
  if (slice.some((part) => internal.consumed.has(part.index))) {
616
1042
  continue;
617
1043
  }
618
1044
  const phrase = slice.map((part) => part.lower).join(" ");
619
- const customCode = customRouteMap?.get(phrase);
1045
+ const customCode = customRouteMap === null || customRouteMap === void 0 ? void 0 : customRouteMap.get(phrase);
620
1046
  const synonym = customCode
621
- ? { code: customCode, text: ROUTE_TEXT[customCode] }
622
- : DEFAULT_ROUTE_SYNONYMS[phrase];
1047
+ ? { code: customCode, text: maps_1.ROUTE_TEXT[customCode] }
1048
+ : maps_1.DEFAULT_ROUTE_SYNONYMS[phrase];
623
1049
  if (synonym) {
624
1050
  setRoute(internal, synonym.code, synonym.text);
625
1051
  for (const part of slice) {
626
1052
  mark(internal.consumed, part);
1053
+ if (BODY_SITE_HINTS.has(part.lower)) {
1054
+ internal.siteTokenIndices.add(part.index);
1055
+ }
627
1056
  }
628
1057
  return true;
629
1058
  }
@@ -635,12 +1064,13 @@ export function parseInternal(input, options) {
635
1064
  if (internal.consumed.has(token.index)) {
636
1065
  continue;
637
1066
  }
1067
+ const normalizedLower = normalizeTokenLower(token);
638
1068
  if (token.lower === "bld" || token.lower === "b-l-d") {
639
- const check = checkDiscouraged(token.original, options);
1069
+ const check = (0, safety_1.checkDiscouraged)(token.original, options);
640
1070
  if (check.warning) {
641
1071
  internal.warnings.push(check.warning);
642
1072
  }
643
- applyWhenToken(internal, token, EventTiming.Meal);
1073
+ applyWhenToken(internal, token, types_1.EventTiming.Meal);
644
1074
  continue;
645
1075
  }
646
1076
  if (token.lower === "q") {
@@ -648,8 +1078,25 @@ export function parseInternal(input, options) {
648
1078
  continue;
649
1079
  }
650
1080
  }
1081
+ if (tryParseNumericCadence(internal, tokens, i)) {
1082
+ continue;
1083
+ }
1084
+ const eyeSite = EYE_SITE_TOKENS[normalizedLower];
1085
+ const treatEyeTokenAsSite = eyeSite
1086
+ ? shouldTreatEyeTokenAsSite(internal, tokens, i, context)
1087
+ : false;
1088
+ if (normalizedLower === "od") {
1089
+ const descriptor = maps_1.TIMING_ABBREVIATIONS.od;
1090
+ if (descriptor &&
1091
+ shouldInterpretOdAsOnceDaily(internal, tokens, i, treatEyeTokenAsSite)) {
1092
+ applyFrequencyDescriptor(internal, token, descriptor, options);
1093
+ continue;
1094
+ }
1095
+ }
651
1096
  // Frequency abbreviation map
652
- const freqDescriptor = TIMING_ABBREVIATIONS[token.lower];
1097
+ const freqDescriptor = normalizedLower === "od"
1098
+ ? undefined
1099
+ : (_d = maps_1.TIMING_ABBREVIATIONS[token.lower]) !== null && _d !== void 0 ? _d : maps_1.TIMING_ABBREVIATIONS[normalizedLower];
653
1100
  if (freqDescriptor) {
654
1101
  applyFrequencyDescriptor(internal, token, freqDescriptor, options);
655
1102
  continue;
@@ -660,34 +1107,34 @@ export function parseInternal(input, options) {
660
1107
  // Event timing tokens
661
1108
  if (token.lower === "pc" || token.lower === "ac") {
662
1109
  parseMealContext(internal, tokens, i, token.lower === "pc"
663
- ? EventTiming["After Meal"]
664
- : EventTiming["Before Meal"]);
1110
+ ? types_1.EventTiming["After Meal"]
1111
+ : types_1.EventTiming["Before Meal"]);
665
1112
  continue;
666
1113
  }
667
1114
  const nextToken = tokens[i + 1];
668
1115
  if (nextToken && !internal.consumed.has(nextToken.index)) {
669
1116
  const combo = `${token.lower} ${nextToken.lower}`;
670
- const comboWhen = COMBO_EVENT_TIMINGS[combo] ?? EVENT_TIMING_TOKENS[combo];
1117
+ const comboWhen = (_e = COMBO_EVENT_TIMINGS[combo]) !== null && _e !== void 0 ? _e : maps_1.EVENT_TIMING_TOKENS[combo];
671
1118
  if (comboWhen) {
672
1119
  applyWhenToken(internal, token, comboWhen);
673
1120
  mark(internal.consumed, nextToken);
674
1121
  continue;
675
1122
  }
676
1123
  }
677
- const customWhen = options?.whenMap?.[token.lower];
1124
+ const customWhen = (_f = options === null || options === void 0 ? void 0 : options.whenMap) === null || _f === void 0 ? void 0 : _f[token.lower];
678
1125
  if (customWhen) {
679
1126
  applyWhenToken(internal, token, customWhen);
680
1127
  continue;
681
1128
  }
682
- const whenCode = EVENT_TIMING_TOKENS[token.lower];
1129
+ const whenCode = maps_1.EVENT_TIMING_TOKENS[token.lower];
683
1130
  if (whenCode) {
684
1131
  applyWhenToken(internal, token, whenCode);
685
1132
  continue;
686
1133
  }
687
1134
  // Day of week
688
- const day = DAY_OF_WEEK_TOKENS[token.lower];
1135
+ const day = maps_1.DAY_OF_WEEK_TOKENS[token.lower];
689
1136
  if (day) {
690
- if (!internal.dayOfWeek.includes(day)) {
1137
+ if (!(0, array_1.arrayIncludes)(internal.dayOfWeek, day)) {
691
1138
  internal.dayOfWeek.push(day);
692
1139
  }
693
1140
  mark(internal.consumed, token);
@@ -697,9 +1144,9 @@ export function parseInternal(input, options) {
697
1144
  if (tryRouteSynonym(i)) {
698
1145
  continue;
699
1146
  }
700
- const eyeSite = EYE_SITE_TOKENS[token.lower];
701
- if (eyeSite) {
1147
+ if (eyeSite && treatEyeTokenAsSite) {
702
1148
  internal.siteText = eyeSite.site;
1149
+ internal.siteSource = "abbreviation";
703
1150
  if (eyeSite.route && !internal.routeCode) {
704
1151
  setRoute(internal, eyeSite.route);
705
1152
  }
@@ -750,7 +1197,7 @@ export function parseInternal(input, options) {
750
1197
  continue;
751
1198
  }
752
1199
  // Words for frequency
753
- const wordFreq = WORD_FREQUENCIES[token.lower];
1200
+ const wordFreq = maps_1.WORD_FREQUENCIES[token.lower];
754
1201
  if (wordFreq) {
755
1202
  internal.frequency = wordFreq.frequency;
756
1203
  internal.period = 1;
@@ -759,7 +1206,7 @@ export function parseInternal(input, options) {
759
1206
  continue;
760
1207
  }
761
1208
  // Skip generic connectors
762
- if (token.lower === "per" || token.lower === "a" || token.lower === "every") {
1209
+ if (token.lower === "per" || token.lower === "a" || token.lower === "every" || token.lower === "each") {
763
1210
  mark(internal.consumed, token);
764
1211
  continue;
765
1212
  }
@@ -778,13 +1225,19 @@ export function parseInternal(input, options) {
778
1225
  }
779
1226
  }
780
1227
  if (internal.unit === undefined) {
781
- internal.unit = inferUnitFromContext(context);
1228
+ internal.unit = (0, context_1.inferUnitFromContext)(context);
1229
+ }
1230
+ if (internal.unit === undefined) {
1231
+ const fallbackUnit = inferUnitFromRouteHints(internal);
1232
+ if (fallbackUnit) {
1233
+ internal.unit = fallbackUnit;
1234
+ }
782
1235
  }
783
1236
  // Frequency defaults when timing code implies it
784
1237
  if (internal.frequency === undefined &&
785
1238
  internal.period === undefined &&
786
1239
  internal.timingCode) {
787
- const descriptor = TIMING_ABBREVIATIONS[internal.timingCode.toLowerCase()];
1240
+ const descriptor = maps_1.TIMING_ABBREVIATIONS[internal.timingCode.toLowerCase()];
788
1241
  if (descriptor) {
789
1242
  if (descriptor.frequency !== undefined) {
790
1243
  internal.frequency = descriptor.frequency;
@@ -804,7 +1257,7 @@ export function parseInternal(input, options) {
804
1257
  }
805
1258
  if (!internal.timingCode &&
806
1259
  internal.frequency !== undefined &&
807
- internal.periodUnit === FhirPeriodUnit.Day &&
1260
+ internal.periodUnit === types_1.FhirPeriodUnit.Day &&
808
1261
  (internal.period === undefined || internal.period === 1)) {
809
1262
  if (internal.frequency === 2) {
810
1263
  internal.timingCode = "BID";
@@ -820,19 +1273,76 @@ export function parseInternal(input, options) {
820
1273
  expandMealTimings(internal, options);
821
1274
  // Determine site text from leftover tokens (excluding PRN reason tokens)
822
1275
  const leftoverTokens = tokens.filter((t) => !internal.consumed.has(t.index));
823
- if (leftoverTokens.length > 0) {
824
- const siteCandidates = leftoverTokens.filter((t) => BODY_SITE_HINTS.has(t.lower));
825
- if (siteCandidates.length > 0) {
826
- const indices = new Set(siteCandidates.map((t) => t.index));
827
- const words = [];
828
- for (const token of leftoverTokens) {
829
- if (indices.has(token.index) || BODY_SITE_HINTS.has(token.lower)) {
830
- words.push(token.original);
831
- mark(internal.consumed, token);
1276
+ const siteCandidateIndices = new Set();
1277
+ for (const token of leftoverTokens) {
1278
+ if (BODY_SITE_HINTS.has(token.lower)) {
1279
+ siteCandidateIndices.add(token.index);
1280
+ }
1281
+ }
1282
+ for (const idx of internal.siteTokenIndices) {
1283
+ siteCandidateIndices.add(idx);
1284
+ }
1285
+ if (siteCandidateIndices.size > 0) {
1286
+ const indicesToInclude = new Set(siteCandidateIndices);
1287
+ for (const idx of siteCandidateIndices) {
1288
+ let prev = idx - 1;
1289
+ while (prev >= 0) {
1290
+ const token = tokens[prev];
1291
+ if (!token) {
1292
+ break;
1293
+ }
1294
+ const lower = token.lower;
1295
+ if (SITE_CONNECTORS.has(lower) || BODY_SITE_HINTS.has(lower)) {
1296
+ indicesToInclude.add(token.index);
1297
+ prev -= 1;
1298
+ continue;
832
1299
  }
1300
+ break;
833
1301
  }
834
- if (words.length > 0) {
835
- internal.siteText = words.join(" ");
1302
+ let next = idx + 1;
1303
+ while (next < tokens.length) {
1304
+ const token = tokens[next];
1305
+ if (!token) {
1306
+ break;
1307
+ }
1308
+ const lower = token.lower;
1309
+ if (SITE_CONNECTORS.has(lower) || BODY_SITE_HINTS.has(lower)) {
1310
+ indicesToInclude.add(token.index);
1311
+ next += 1;
1312
+ continue;
1313
+ }
1314
+ break;
1315
+ }
1316
+ }
1317
+ const sortedIndices = Array.from(indicesToInclude).sort((a, b) => a - b);
1318
+ const displayWords = [];
1319
+ for (const index of sortedIndices) {
1320
+ const token = tokens[index];
1321
+ if (!token) {
1322
+ continue;
1323
+ }
1324
+ const lower = token.lower;
1325
+ if (!SITE_CONNECTORS.has(lower) && !SITE_FILLER_WORDS.has(lower)) {
1326
+ displayWords.push(token.original);
1327
+ }
1328
+ mark(internal.consumed, token);
1329
+ }
1330
+ const normalizedSite = displayWords
1331
+ .filter((word) => !SITE_CONNECTORS.has(word.trim().toLowerCase()))
1332
+ .join(" ")
1333
+ .trim();
1334
+ if (normalizedSite) {
1335
+ internal.siteText = normalizedSite;
1336
+ if (!internal.siteSource) {
1337
+ internal.siteSource = "text";
1338
+ }
1339
+ }
1340
+ }
1341
+ if (!internal.routeCode && internal.siteText) {
1342
+ for (const { pattern, route } of SITE_UNIT_ROUTE_HINTS) {
1343
+ if (pattern.test(internal.siteText)) {
1344
+ setRoute(internal, route);
1345
+ break;
836
1346
  }
837
1347
  }
838
1348
  }
@@ -851,20 +1361,57 @@ export function parseInternal(input, options) {
851
1361
  internal.asNeededReason = reasonTokens.join(" ");
852
1362
  }
853
1363
  }
854
- if (internal.routeCode === RouteCode["Intravitreal route (qualifier value)"] &&
1364
+ if (internal.routeCode === types_1.RouteCode["Intravitreal route (qualifier value)"] &&
855
1365
  (!internal.siteText || !/eye/i.test(internal.siteText))) {
856
1366
  internal.warnings.push("Intravitreal administrations require an eye site (e.g., OD/OS/OU).");
857
1367
  }
858
1368
  return internal;
859
1369
  }
860
1370
  function normalizeUnit(token, options) {
861
- const override = options?.unitMap?.[token];
1371
+ var _a;
1372
+ const override = (_a = options === null || options === void 0 ? void 0 : options.unitMap) === null || _a === void 0 ? void 0 : _a[token];
862
1373
  if (override) {
863
1374
  return override;
864
1375
  }
865
- const defaultUnit = DEFAULT_UNIT_SYNONYMS[token];
1376
+ const defaultUnit = maps_1.DEFAULT_UNIT_SYNONYMS[token];
866
1377
  if (defaultUnit) {
867
1378
  return defaultUnit;
868
1379
  }
869
1380
  return undefined;
870
1381
  }
1382
+ function inferUnitFromRouteHints(internal) {
1383
+ if (internal.routeCode) {
1384
+ const unit = maps_1.DEFAULT_UNIT_BY_ROUTE[internal.routeCode];
1385
+ if (unit) {
1386
+ return unit;
1387
+ }
1388
+ }
1389
+ if (internal.routeText) {
1390
+ const normalized = internal.routeText.trim().toLowerCase();
1391
+ const synonym = maps_1.DEFAULT_ROUTE_SYNONYMS[normalized];
1392
+ if (synonym) {
1393
+ const unit = maps_1.DEFAULT_UNIT_BY_ROUTE[synonym.code];
1394
+ if (unit) {
1395
+ return unit;
1396
+ }
1397
+ }
1398
+ }
1399
+ if (internal.siteText) {
1400
+ const unit = inferUnitFromSiteText(internal.siteText);
1401
+ if (unit) {
1402
+ return unit;
1403
+ }
1404
+ }
1405
+ return undefined;
1406
+ }
1407
+ function inferUnitFromSiteText(siteText) {
1408
+ for (const { pattern, route } of SITE_UNIT_ROUTE_HINTS) {
1409
+ if (pattern.test(siteText)) {
1410
+ const unit = maps_1.DEFAULT_UNIT_BY_ROUTE[route];
1411
+ if (unit) {
1412
+ return unit;
1413
+ }
1414
+ }
1415
+ }
1416
+ return undefined;
1417
+ }