ezmedicationinput 0.1.1 → 0.1.3

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,492 @@ 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 HOUSEHOLD_VOLUME_UNIT_SET = new Set(maps_1.HOUSEHOLD_VOLUME_UNITS.map((unit) => unit.toLowerCase()));
86
+ const OCULAR_DIRECTION_WORDS = new Set([
87
+ "left",
88
+ "right",
89
+ "both",
90
+ "either",
91
+ "each",
92
+ "bilateral"
93
+ ]);
94
+ const OCULAR_SITE_WORDS = new Set([
95
+ "eye",
96
+ "eyes",
97
+ "eyelid",
98
+ "eyelids",
99
+ "ocular",
100
+ "ophthalmic",
101
+ "oculus"
35
102
  ]);
36
103
  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
104
+ "early morning": types_1.EventTiming["Early Morning"],
105
+ "late morning": types_1.EventTiming["Late Morning"],
106
+ "early afternoon": types_1.EventTiming["Early Afternoon"],
107
+ "late afternoon": types_1.EventTiming["Late Afternoon"],
108
+ "early evening": types_1.EventTiming["Early Evening"],
109
+ "late evening": types_1.EventTiming["Late Evening"],
110
+ "after sleep": types_1.EventTiming["After Sleep"],
111
+ "upon waking": types_1.EventTiming.Wake
45
112
  };
46
113
  // Tracking explicit breakfast/lunch/dinner markers lets the meal-expansion
47
114
  // logic bail early when the clinician already specified precise events.
48
115
  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
116
+ types_1.EventTiming["Before Breakfast"],
117
+ types_1.EventTiming["Before Lunch"],
118
+ types_1.EventTiming["Before Dinner"],
119
+ types_1.EventTiming["After Breakfast"],
120
+ types_1.EventTiming["After Lunch"],
121
+ types_1.EventTiming["After Dinner"],
122
+ types_1.EventTiming.Breakfast,
123
+ types_1.EventTiming.Lunch,
124
+ types_1.EventTiming.Dinner
58
125
  ]);
59
126
  // Ocular shorthand tokens commonly used in ophthalmic sigs.
60
127
  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"] },
128
+ od: { site: "right eye", route: types_1.RouteCode["Ophthalmic route"] },
129
+ re: { site: "right eye", route: types_1.RouteCode["Ophthalmic route"] },
130
+ os: { site: "left eye", route: types_1.RouteCode["Ophthalmic route"] },
131
+ le: { site: "left eye", route: types_1.RouteCode["Ophthalmic route"] },
132
+ ou: { site: "both eyes", route: types_1.RouteCode["Ophthalmic route"] },
133
+ be: { site: "both eyes", route: types_1.RouteCode["Ophthalmic route"] },
67
134
  vod: {
68
135
  site: "right eye",
69
- route: RouteCode["Intravitreal route (qualifier value)"]
136
+ route: types_1.RouteCode["Intravitreal route (qualifier value)"]
70
137
  },
71
138
  vos: {
72
139
  site: "left eye",
73
- route: RouteCode["Intravitreal route (qualifier value)"]
140
+ route: types_1.RouteCode["Intravitreal route (qualifier value)"]
74
141
  },
75
142
  ivtod: {
76
143
  site: "right eye",
77
- route: RouteCode["Intravitreal route (qualifier value)"]
144
+ route: types_1.RouteCode["Intravitreal route (qualifier value)"]
78
145
  },
79
146
  ivtre: {
80
147
  site: "right eye",
81
- route: RouteCode["Intravitreal route (qualifier value)"]
148
+ route: types_1.RouteCode["Intravitreal route (qualifier value)"]
82
149
  },
83
150
  ivtos: {
84
151
  site: "left eye",
85
- route: RouteCode["Intravitreal route (qualifier value)"]
152
+ route: types_1.RouteCode["Intravitreal route (qualifier value)"]
86
153
  },
87
154
  ivtle: {
88
155
  site: "left eye",
89
- route: RouteCode["Intravitreal route (qualifier value)"]
156
+ route: types_1.RouteCode["Intravitreal route (qualifier value)"]
90
157
  },
91
158
  ivtou: {
92
159
  site: "both eyes",
93
- route: RouteCode["Intravitreal route (qualifier value)"]
160
+ route: types_1.RouteCode["Intravitreal route (qualifier value)"]
94
161
  },
95
162
  ivtbe: {
96
163
  site: "both eyes",
97
- route: RouteCode["Intravitreal route (qualifier value)"]
164
+ route: types_1.RouteCode["Intravitreal route (qualifier value)"]
98
165
  }
99
166
  };
100
- export function tokenize(input) {
167
+ const OPHTHALMIC_ROUTE_CODES = new Set([
168
+ types_1.RouteCode["Ophthalmic route"],
169
+ types_1.RouteCode["Intravitreal route (qualifier value)"]
170
+ ]);
171
+ const OPHTHALMIC_CONTEXT_TOKENS = new Set([
172
+ "drop",
173
+ "drops",
174
+ "gtt",
175
+ "gtts",
176
+ "eye",
177
+ "eyes",
178
+ "eyelid",
179
+ "eyelids",
180
+ "ocular",
181
+ "ophthalmic",
182
+ "ophth",
183
+ "oculus",
184
+ "os",
185
+ "ou",
186
+ "re",
187
+ "le",
188
+ "be"
189
+ ]);
190
+ function normalizeTokenLower(token) {
191
+ return token.lower.replace(/\./g, "");
192
+ }
193
+ function hasOphthalmicContextHint(tokens, index) {
194
+ for (let offset = -3; offset <= 3; offset++) {
195
+ if (offset === 0) {
196
+ continue;
197
+ }
198
+ const neighbor = tokens[index + offset];
199
+ if (!neighbor) {
200
+ continue;
201
+ }
202
+ const normalized = normalizeTokenLower(neighbor);
203
+ if (OPHTHALMIC_CONTEXT_TOKENS.has(normalized) || normalized.includes("eye")) {
204
+ return true;
205
+ }
206
+ }
207
+ return false;
208
+ }
209
+ function shouldInterpretOdAsOnceDaily(internal, tokens, index, treatAsSite) {
210
+ var _a;
211
+ if (treatAsSite) {
212
+ return false;
213
+ }
214
+ const hasCadenceAssigned = internal.frequency !== undefined ||
215
+ internal.frequencyMax !== undefined ||
216
+ internal.period !== undefined ||
217
+ internal.periodMax !== undefined ||
218
+ internal.timingCode !== undefined;
219
+ const hasPriorSiteContext = hasBodySiteContextBefore(internal, tokens, index);
220
+ const hasUpcomingSiteContext = hasBodySiteContextAfter(internal, tokens, index);
221
+ const previous = tokens[index - 1];
222
+ const previousNormalized = previous ? normalizeTokenLower(previous) : undefined;
223
+ const previousIsOd = previousNormalized === "od";
224
+ const previousConsumed = previousIsOd && internal.consumed.has(previous.index);
225
+ const previousOdProvidedSite = previousConsumed && /eye/i.test((_a = internal.siteText) !== null && _a !== void 0 ? _a : "");
226
+ if (previousOdProvidedSite) {
227
+ return true;
228
+ }
229
+ const previousEyeToken = previousNormalized && previousNormalized !== "od"
230
+ ? EYE_SITE_TOKENS[previousNormalized]
231
+ : undefined;
232
+ if (previousEyeToken && internal.consumed.has(previous.index)) {
233
+ return true;
234
+ }
235
+ if (previousNormalized === "od" &&
236
+ internal.siteSource === "abbreviation" &&
237
+ internal.siteText &&
238
+ /eye/i.test(internal.siteText)) {
239
+ return true;
240
+ }
241
+ if (hasPriorSiteContext || hasUpcomingSiteContext) {
242
+ return !hasCadenceAssigned;
243
+ }
244
+ if (hasCadenceAssigned) {
245
+ return false;
246
+ }
247
+ if (internal.routeCode && !OPHTHALMIC_ROUTE_CODES.has(internal.routeCode)) {
248
+ return true;
249
+ }
250
+ if (internal.unit && internal.unit !== "drop") {
251
+ return true;
252
+ }
253
+ if (internal.siteText && !/eye/i.test(internal.siteText)) {
254
+ return true;
255
+ }
256
+ const hasNonOdToken = tokens.some((token, tokenIndex) => {
257
+ if (tokenIndex === index) {
258
+ return false;
259
+ }
260
+ return normalizeTokenLower(token) !== "od";
261
+ });
262
+ if (!hasNonOdToken) {
263
+ return false;
264
+ }
265
+ const ophthalmicContext = hasOphthalmicContextHint(tokens, index) ||
266
+ (internal.routeCode !== undefined && OPHTHALMIC_ROUTE_CODES.has(internal.routeCode)) ||
267
+ (internal.siteText !== undefined && /eye/i.test(internal.siteText));
268
+ if (ophthalmicContext && hasSpelledOcularSiteBefore(tokens, index)) {
269
+ return true;
270
+ }
271
+ return !ophthalmicContext;
272
+ }
273
+ function hasBodySiteContextBefore(internal, tokens, index) {
274
+ const currentToken = tokens[index];
275
+ const currentTokenIndex = currentToken ? currentToken.index : index;
276
+ if (internal.siteText) {
277
+ return true;
278
+ }
279
+ for (const tokenIndex of internal.siteTokenIndices) {
280
+ if (tokenIndex < currentTokenIndex) {
281
+ return true;
282
+ }
283
+ }
284
+ for (let i = 0; i < index; i++) {
285
+ const token = tokens[i];
286
+ if (!token) {
287
+ continue;
288
+ }
289
+ if (internal.consumed.has(token.index)) {
290
+ if (internal.siteTokenIndices.has(token.index) && token.index < currentTokenIndex) {
291
+ return true;
292
+ }
293
+ continue;
294
+ }
295
+ const normalized = normalizeTokenLower(token);
296
+ if (BODY_SITE_HINTS.has(normalized)) {
297
+ return true;
298
+ }
299
+ if (EYE_SITE_TOKENS[normalized]) {
300
+ return true;
301
+ }
302
+ }
303
+ return false;
304
+ }
305
+ function hasBodySiteContextAfter(internal, tokens, index) {
306
+ const currentToken = tokens[index];
307
+ const currentTokenIndex = currentToken ? currentToken.index : index;
308
+ for (const tokenIndex of internal.siteTokenIndices) {
309
+ if (tokenIndex > currentTokenIndex) {
310
+ return true;
311
+ }
312
+ }
313
+ let seenConnector = false;
314
+ for (let i = index + 1; i < tokens.length; i++) {
315
+ const token = tokens[i];
316
+ if (!token) {
317
+ continue;
318
+ }
319
+ if (internal.consumed.has(token.index)) {
320
+ if (internal.siteTokenIndices.has(token.index) && token.index > currentTokenIndex) {
321
+ return true;
322
+ }
323
+ continue;
324
+ }
325
+ const normalized = normalizeTokenLower(token);
326
+ if (SITE_CONNECTORS.has(normalized)) {
327
+ seenConnector = true;
328
+ continue;
329
+ }
330
+ if (SITE_FILLER_WORDS.has(normalized)) {
331
+ continue;
332
+ }
333
+ if (BODY_SITE_HINTS.has(normalized)) {
334
+ return true;
335
+ }
336
+ if (seenConnector) {
337
+ break;
338
+ }
339
+ if (!seenConnector) {
340
+ break;
341
+ }
342
+ }
343
+ return false;
344
+ }
345
+ function hasSpelledOcularSiteBefore(tokens, index) {
346
+ let hasOcularWord = false;
347
+ let hasDirectionalCue = false;
348
+ for (let i = 0; i < index; i++) {
349
+ const token = tokens[i];
350
+ if (!token) {
351
+ continue;
352
+ }
353
+ const normalized = normalizeTokenLower(token);
354
+ if (SITE_CONNECTORS.has(normalized) || OCULAR_DIRECTION_WORDS.has(normalized)) {
355
+ hasDirectionalCue = true;
356
+ }
357
+ if (OCULAR_SITE_WORDS.has(normalized) || normalized.includes("eye")) {
358
+ hasOcularWord = true;
359
+ }
360
+ if (hasDirectionalCue && hasOcularWord) {
361
+ return true;
362
+ }
363
+ }
364
+ return false;
365
+ }
366
+ function shouldTreatEyeTokenAsSite(internal, tokens, index, context) {
367
+ var _a;
368
+ const currentToken = tokens[index];
369
+ const normalizedSelf = normalizeTokenLower(currentToken);
370
+ const eyeMeta = EYE_SITE_TOKENS[normalizedSelf];
371
+ if (internal.routeCode && !OPHTHALMIC_ROUTE_CODES.has(internal.routeCode)) {
372
+ return false;
373
+ }
374
+ if (internal.siteText) {
375
+ return false;
376
+ }
377
+ if (internal.siteSource === "abbreviation") {
378
+ return false;
379
+ }
380
+ const dosageForm = (_a = context === null || context === void 0 ? void 0 : context.dosageForm) === null || _a === void 0 ? void 0 : _a.toLowerCase();
381
+ const contextImpliesOphthalmic = Boolean(dosageForm && /(eye|ophth|ocular|intravit)/i.test(dosageForm));
382
+ const eyeRouteImpliesOphthalmic = (eyeMeta === null || eyeMeta === void 0 ? void 0 : eyeMeta.route) === types_1.RouteCode["Intravitreal route (qualifier value)"];
383
+ const ophthalmicContext = hasOphthalmicContextHint(tokens, index) ||
384
+ (internal.routeCode !== undefined && OPHTHALMIC_ROUTE_CODES.has(internal.routeCode)) ||
385
+ contextImpliesOphthalmic ||
386
+ eyeRouteImpliesOphthalmic;
387
+ if (hasBodySiteContextAfter(internal, tokens, index)) {
388
+ return false;
389
+ }
390
+ if (!ophthalmicContext) {
391
+ const hasOtherActiveTokens = tokens.some((token, tokenIndex) => tokenIndex !== index && !internal.consumed.has(token.index));
392
+ const onlyEyeTokens = tokens.every((token, tokenIndex) => {
393
+ if (tokenIndex === index || internal.consumed.has(token.index)) {
394
+ return true;
395
+ }
396
+ return normalizeTokenLower(token) === "od";
397
+ });
398
+ if (!hasOtherActiveTokens) {
399
+ return internal.unit === undefined && internal.routeCode === undefined;
400
+ }
401
+ if (onlyEyeTokens) {
402
+ return true;
403
+ }
404
+ return false;
405
+ }
406
+ for (let i = 0; i < index; i++) {
407
+ const candidate = tokens[i];
408
+ if (internal.consumed.has(candidate.index)) {
409
+ continue;
410
+ }
411
+ const normalized = normalizeTokenLower(candidate);
412
+ if (SITE_CONNECTORS.has(normalized)) {
413
+ continue;
414
+ }
415
+ if (BODY_SITE_HINTS.has(normalized)) {
416
+ return false;
417
+ }
418
+ if (EYE_SITE_TOKENS[normalized]) {
419
+ return false;
420
+ }
421
+ if (maps_1.DEFAULT_ROUTE_SYNONYMS[normalized]) {
422
+ return false;
423
+ }
424
+ }
425
+ return true;
426
+ }
427
+ function tryParseNumericCadence(internal, tokens, index) {
428
+ const token = tokens[index];
429
+ if (!/^[0-9]+(?:\.[0-9]+)?$/.test(token.lower)) {
430
+ return false;
431
+ }
432
+ if (internal.frequency !== undefined ||
433
+ internal.frequencyMax !== undefined ||
434
+ internal.period !== undefined ||
435
+ internal.periodMax !== undefined) {
436
+ return false;
437
+ }
438
+ let nextIndex = index + 1;
439
+ const connectors = [];
440
+ while (true) {
441
+ const connector = tokens[nextIndex];
442
+ if (!connector || internal.consumed.has(connector.index)) {
443
+ break;
444
+ }
445
+ const normalized = normalizeTokenLower(connector);
446
+ if (normalized === "per" || normalized === "a" || normalized === "each" || normalized === "every") {
447
+ connectors.push(connector);
448
+ nextIndex += 1;
449
+ continue;
450
+ }
451
+ break;
452
+ }
453
+ if (!connectors.length) {
454
+ return false;
455
+ }
456
+ const unitToken = tokens[nextIndex];
457
+ if (!unitToken || internal.consumed.has(unitToken.index)) {
458
+ return false;
459
+ }
460
+ const unitCode = mapIntervalUnit(normalizeTokenLower(unitToken));
461
+ if (!unitCode) {
462
+ return false;
463
+ }
464
+ const value = parseFloat(token.original);
465
+ if (!Number.isFinite(value)) {
466
+ return false;
467
+ }
468
+ internal.frequency = value;
469
+ internal.period = 1;
470
+ internal.periodUnit = unitCode;
471
+ if (value === 1 && unitCode === types_1.FhirPeriodUnit.Day && !internal.timingCode) {
472
+ internal.timingCode = "QD";
473
+ }
474
+ mark(internal.consumed, token);
475
+ for (const connector of connectors) {
476
+ mark(internal.consumed, connector);
477
+ }
478
+ mark(internal.consumed, unitToken);
479
+ return true;
480
+ }
481
+ const SITE_UNIT_ROUTE_HINTS = [
482
+ { pattern: /\beye(s)?\b/i, route: types_1.RouteCode["Ophthalmic route"] },
483
+ { pattern: /\beyelid(s)?\b/i, route: types_1.RouteCode["Ophthalmic route"] },
484
+ { pattern: /\bintravitreal\b/i, route: types_1.RouteCode["Intravitreal route (qualifier value)"] },
485
+ { pattern: /\bear(s)?\b/i, route: types_1.RouteCode["Otic route"] },
486
+ { pattern: /\bnostril(s)?\b/i, route: types_1.RouteCode["Nasal route"] },
487
+ { pattern: /\bnares?\b/i, route: types_1.RouteCode["Nasal route"] },
488
+ { pattern: /\bnose\b/i, route: types_1.RouteCode["Nasal route"] },
489
+ { pattern: /\bmouth\b/i, route: types_1.RouteCode["Oral route"] },
490
+ { pattern: /\boral\b/i, route: types_1.RouteCode["Oral route"] },
491
+ { pattern: /\bunder (the )?tongue\b/i, route: types_1.RouteCode["Sublingual route"] },
492
+ { pattern: /\btongue\b/i, route: types_1.RouteCode["Sublingual route"] },
493
+ { pattern: /\bcheek(s)?\b/i, route: types_1.RouteCode["Buccal route"] },
494
+ { pattern: /\blung(s)?\b/i, route: types_1.RouteCode["Respiratory tract route (qualifier value)"] },
495
+ { pattern: /\brespiratory tract\b/i, route: types_1.RouteCode["Respiratory tract route (qualifier value)"] },
496
+ { pattern: /\bskin\b/i, route: types_1.RouteCode["Topical route"] },
497
+ { pattern: /\bscalp\b/i, route: types_1.RouteCode["Topical route"] },
498
+ { pattern: /\bface\b/i, route: types_1.RouteCode["Topical route"] },
499
+ { pattern: /\bhand(s)?\b/i, route: types_1.RouteCode["Topical route"] },
500
+ { pattern: /(\bfoot\b|\bfeet\b)/i, route: types_1.RouteCode["Topical route"] },
501
+ { pattern: /\belbow(s)?\b/i, route: types_1.RouteCode["Topical route"] },
502
+ { pattern: /\bknee(s)?\b/i, route: types_1.RouteCode["Topical route"] },
503
+ { pattern: /\bleg(s)?\b/i, route: types_1.RouteCode["Topical route"] },
504
+ { pattern: /\barm(s)?\b/i, route: types_1.RouteCode["Topical route"] },
505
+ { pattern: /\bpatch(es)?\b/i, route: types_1.RouteCode["Transdermal route"] },
506
+ { pattern: /\babdomen\b/i, route: types_1.RouteCode["Subcutaneous route"] },
507
+ { pattern: /\bbelly\b/i, route: types_1.RouteCode["Subcutaneous route"] },
508
+ { pattern: /\bstomach\b/i, route: types_1.RouteCode["Subcutaneous route"] },
509
+ { pattern: /\bthigh(s)?\b/i, route: types_1.RouteCode["Subcutaneous route"] },
510
+ { pattern: /\bupper arm\b/i, route: types_1.RouteCode["Subcutaneous route"] },
511
+ { pattern: /\bbuttock(s)?\b/i, route: types_1.RouteCode["Intramuscular route"] },
512
+ { pattern: /\bglute(al)?\b/i, route: types_1.RouteCode["Intramuscular route"] },
513
+ { pattern: /\bdeltoid\b/i, route: types_1.RouteCode["Intramuscular route"] },
514
+ { pattern: /\bmuscle(s)?\b/i, route: types_1.RouteCode["Intramuscular route"] },
515
+ { pattern: /\bvein(s)?\b/i, route: types_1.RouteCode["Intravenous route"] },
516
+ { pattern: /\brectum\b/i, route: types_1.RouteCode["Per rectum"] },
517
+ { pattern: /\banus\b/i, route: types_1.RouteCode["Per rectum"] },
518
+ { pattern: /\brectal\b/i, route: types_1.RouteCode["Per rectum"] },
519
+ { pattern: /\bvagina\b/i, route: types_1.RouteCode["Per vagina"] },
520
+ { pattern: /\bvaginal\b/i, route: types_1.RouteCode["Per vagina"] }
521
+ ];
522
+ function tokenize(input) {
101
523
  const separators = /[(),]/g;
102
524
  let normalized = input.trim().replace(separators, " ");
525
+ 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
526
  normalized = normalized.replace(/(\d+)\s*\/\s*(\d+)/g, (match, num, den) => {
104
527
  const numerator = parseFloat(num);
105
528
  const denominator = parseFloat(den);
@@ -154,7 +577,7 @@ function mark(consumed, token) {
154
577
  consumed.add(token.index);
155
578
  }
156
579
  function addWhen(target, code) {
157
- if (!target.includes(code)) {
580
+ if (!(0, array_1.arrayIncludes)(target, code)) {
158
581
  target.push(code);
159
582
  }
160
583
  }
@@ -167,74 +590,184 @@ function removeWhen(target, code) {
167
590
  index = target.indexOf(code);
168
591
  }
169
592
  }
593
+ const DEFAULT_EVENT_TIMING_WEIGHTS = {
594
+ [types_1.EventTiming.Immediate]: 0,
595
+ [types_1.EventTiming.Wake]: 6 * 3600,
596
+ [types_1.EventTiming["After Sleep"]]: 6 * 3600 + 15 * 60,
597
+ [types_1.EventTiming["Early Morning"]]: 7 * 3600,
598
+ [types_1.EventTiming["Before Meal"]]: 7 * 3600 + 30 * 60,
599
+ [types_1.EventTiming["Before Breakfast"]]: 7 * 3600 + 45 * 60,
600
+ [types_1.EventTiming.Morning]: 8 * 3600,
601
+ [types_1.EventTiming.Breakfast]: 8 * 3600 + 15 * 60,
602
+ [types_1.EventTiming.Meal]: 8 * 3600 + 30 * 60,
603
+ [types_1.EventTiming["After Breakfast"]]: 9 * 3600,
604
+ [types_1.EventTiming["After Meal"]]: 9 * 3600 + 15 * 60,
605
+ [types_1.EventTiming["Late Morning"]]: 10 * 3600 + 30 * 60,
606
+ [types_1.EventTiming["Before Lunch"]]: 11 * 3600 + 45 * 60,
607
+ [types_1.EventTiming.Noon]: 12 * 3600,
608
+ [types_1.EventTiming.Lunch]: 12 * 3600 + 15 * 60,
609
+ [types_1.EventTiming["After Lunch"]]: 12 * 3600 + 45 * 60,
610
+ [types_1.EventTiming["Early Afternoon"]]: 13 * 3600 + 30 * 60,
611
+ [types_1.EventTiming.Afternoon]: 15 * 3600,
612
+ [types_1.EventTiming["Late Afternoon"]]: 16 * 3600 + 30 * 60,
613
+ [types_1.EventTiming["Before Dinner"]]: 17 * 3600 + 30 * 60,
614
+ [types_1.EventTiming.Dinner]: 18 * 3600,
615
+ [types_1.EventTiming["After Dinner"]]: 19 * 3600,
616
+ [types_1.EventTiming["Early Evening"]]: 19 * 3600 + 30 * 60,
617
+ [types_1.EventTiming.Evening]: 20 * 3600,
618
+ [types_1.EventTiming["Late Evening"]]: 21 * 3600,
619
+ [types_1.EventTiming.Night]: 22 * 3600,
620
+ [types_1.EventTiming["Before Sleep"]]: 22 * 3600 + 30 * 60,
621
+ };
622
+ function parseClockToSeconds(clock) {
623
+ const match = clock.match(/^(\d{1,2}):(\d{2})(?::(\d{2}))?$/);
624
+ if (!match) {
625
+ return undefined;
626
+ }
627
+ const hour = Number(match[1]);
628
+ const minute = Number(match[2]);
629
+ const second = match[3] ? Number(match[3]) : 0;
630
+ if (!Number.isFinite(hour) ||
631
+ !Number.isFinite(minute) ||
632
+ !Number.isFinite(second) ||
633
+ hour < 0 ||
634
+ hour > 23 ||
635
+ minute < 0 ||
636
+ minute > 59 ||
637
+ second < 0 ||
638
+ second > 59) {
639
+ return undefined;
640
+ }
641
+ return hour * 3600 + minute * 60 + second;
642
+ }
643
+ function computeWhenWeight(code, options) {
644
+ var _a, _b;
645
+ const clock = (_a = options === null || options === void 0 ? void 0 : options.eventClock) === null || _a === void 0 ? void 0 : _a[code];
646
+ if (clock) {
647
+ const seconds = parseClockToSeconds(clock);
648
+ if (seconds !== undefined) {
649
+ return seconds;
650
+ }
651
+ }
652
+ return (_b = DEFAULT_EVENT_TIMING_WEIGHTS[code]) !== null && _b !== void 0 ? _b : 10000;
653
+ }
654
+ function sortWhenValues(internal, options) {
655
+ if (internal.when.length < 2) {
656
+ return;
657
+ }
658
+ const weighted = internal.when.map((code, index) => ({
659
+ code,
660
+ weight: computeWhenWeight(code, options),
661
+ index,
662
+ }));
663
+ weighted.sort((a, b) => {
664
+ if (a.weight !== b.weight) {
665
+ return a.weight - b.weight;
666
+ }
667
+ return a.index - b.index;
668
+ });
669
+ internal.when.splice(0, internal.when.length, ...weighted.map((entry) => entry.code));
670
+ }
170
671
  // Translate the requested expansion context into the appropriate sequence of
171
672
  // EventTiming values (e.g., AC -> ACM/ACD/ACV) for the detected frequency.
172
673
  function computeMealExpansions(base, frequency, pairPreference) {
173
674
  if (frequency < 1 || frequency > 4) {
174
675
  return undefined;
175
676
  }
176
- const bedtime = EventTiming["Before Sleep"];
677
+ const bedtime = types_1.EventTiming["Before Sleep"];
177
678
  const beforePair = pairPreference === "breakfast+lunch"
178
- ? [EventTiming["Before Breakfast"], EventTiming["Before Lunch"]]
179
- : [EventTiming["Before Breakfast"], EventTiming["Before Dinner"]];
679
+ ? [types_1.EventTiming["Before Breakfast"], types_1.EventTiming["Before Lunch"]]
680
+ : [types_1.EventTiming["Before Breakfast"], types_1.EventTiming["Before Dinner"]];
180
681
  const afterPair = pairPreference === "breakfast+lunch"
181
- ? [EventTiming["After Breakfast"], EventTiming["After Lunch"]]
182
- : [EventTiming["After Breakfast"], EventTiming["After Dinner"]];
682
+ ? [types_1.EventTiming["After Breakfast"], types_1.EventTiming["After Lunch"]]
683
+ : [types_1.EventTiming["After Breakfast"], types_1.EventTiming["After Dinner"]];
183
684
  const withPair = pairPreference === "breakfast+lunch"
184
- ? [EventTiming.Breakfast, EventTiming.Lunch]
185
- : [EventTiming.Breakfast, EventTiming.Dinner];
685
+ ? [types_1.EventTiming.Breakfast, types_1.EventTiming.Lunch]
686
+ : [types_1.EventTiming.Breakfast, types_1.EventTiming.Dinner];
186
687
  if (base === "before") {
187
688
  if (frequency === 1)
188
- return [EventTiming["Before Breakfast"]];
689
+ return [types_1.EventTiming["Before Breakfast"]];
189
690
  if (frequency === 2)
190
691
  return beforePair;
191
692
  if (frequency === 3) {
192
693
  return [
193
- EventTiming["Before Breakfast"],
194
- EventTiming["Before Lunch"],
195
- EventTiming["Before Dinner"]
694
+ types_1.EventTiming["Before Breakfast"],
695
+ types_1.EventTiming["Before Lunch"],
696
+ types_1.EventTiming["Before Dinner"]
196
697
  ];
197
698
  }
198
699
  return [
199
- EventTiming["Before Breakfast"],
200
- EventTiming["Before Lunch"],
201
- EventTiming["Before Dinner"],
700
+ types_1.EventTiming["Before Breakfast"],
701
+ types_1.EventTiming["Before Lunch"],
702
+ types_1.EventTiming["Before Dinner"],
202
703
  bedtime
203
704
  ];
204
705
  }
205
706
  if (base === "after") {
206
707
  if (frequency === 1)
207
- return [EventTiming["After Breakfast"]];
708
+ return [types_1.EventTiming["After Breakfast"]];
208
709
  if (frequency === 2)
209
710
  return afterPair;
210
711
  if (frequency === 3) {
211
712
  return [
212
- EventTiming["After Breakfast"],
213
- EventTiming["After Lunch"],
214
- EventTiming["After Dinner"]
713
+ types_1.EventTiming["After Breakfast"],
714
+ types_1.EventTiming["After Lunch"],
715
+ types_1.EventTiming["After Dinner"]
215
716
  ];
216
717
  }
217
718
  return [
218
- EventTiming["After Breakfast"],
219
- EventTiming["After Lunch"],
220
- EventTiming["After Dinner"],
719
+ types_1.EventTiming["After Breakfast"],
720
+ types_1.EventTiming["After Lunch"],
721
+ types_1.EventTiming["After Dinner"],
221
722
  bedtime
222
723
  ];
223
724
  }
224
725
  // base === "with"
225
726
  if (frequency === 1)
226
- return [EventTiming.Breakfast];
727
+ return [types_1.EventTiming.Breakfast];
227
728
  if (frequency === 2)
228
729
  return withPair;
229
730
  if (frequency === 3) {
230
- return [EventTiming.Breakfast, EventTiming.Lunch, EventTiming.Dinner];
731
+ return [types_1.EventTiming.Breakfast, types_1.EventTiming.Lunch, types_1.EventTiming.Dinner];
231
732
  }
232
- return [EventTiming.Breakfast, EventTiming.Lunch, EventTiming.Dinner, bedtime];
733
+ return [types_1.EventTiming.Breakfast, types_1.EventTiming.Lunch, types_1.EventTiming.Dinner, bedtime];
734
+ }
735
+ function reconcileMealTimingSpecificity(internal) {
736
+ if (!internal.when.length) {
737
+ return;
738
+ }
739
+ const convertSpecifics = (base, mappings) => {
740
+ if (!(0, array_1.arrayIncludes)(internal.when, base)) {
741
+ return;
742
+ }
743
+ let replaced = false;
744
+ for (const [general, specific] of mappings) {
745
+ if ((0, array_1.arrayIncludes)(internal.when, general)) {
746
+ removeWhen(internal.when, general);
747
+ addWhen(internal.when, specific);
748
+ replaced = true;
749
+ }
750
+ }
751
+ if (replaced) {
752
+ removeWhen(internal.when, base);
753
+ }
754
+ };
755
+ convertSpecifics(types_1.EventTiming["Before Meal"], [
756
+ [types_1.EventTiming.Breakfast, types_1.EventTiming["Before Breakfast"]],
757
+ [types_1.EventTiming.Lunch, types_1.EventTiming["Before Lunch"]],
758
+ [types_1.EventTiming.Dinner, types_1.EventTiming["Before Dinner"]],
759
+ ]);
760
+ convertSpecifics(types_1.EventTiming["After Meal"], [
761
+ [types_1.EventTiming.Breakfast, types_1.EventTiming["After Breakfast"]],
762
+ [types_1.EventTiming.Lunch, types_1.EventTiming["After Lunch"]],
763
+ [types_1.EventTiming.Dinner, types_1.EventTiming["After Dinner"]],
764
+ ]);
233
765
  }
234
766
  // Optionally replace generic meal tokens with concrete breakfast/lunch/dinner
235
767
  // EventTiming codes when the cadence makes the intent obvious.
236
768
  function expandMealTimings(internal, options) {
237
- if (!options?.smartMealExpansion) {
769
+ var _a;
770
+ if (!(options === null || options === void 0 ? void 0 : options.smartMealExpansion)) {
238
771
  return;
239
772
  }
240
773
  if (!internal.when.length) {
@@ -249,7 +782,7 @@ function expandMealTimings(internal, options) {
249
782
  }
250
783
  if (internal.period !== undefined &&
251
784
  internal.periodUnit !== undefined &&
252
- (internal.periodUnit !== FhirPeriodUnit.Day || internal.period !== 1)) {
785
+ (internal.periodUnit !== types_1.FhirPeriodUnit.Day || internal.period !== 1)) {
253
786
  return;
254
787
  }
255
788
  if (internal.period !== undefined &&
@@ -257,30 +790,30 @@ function expandMealTimings(internal, options) {
257
790
  internal.period !== 1) {
258
791
  return;
259
792
  }
260
- if (internal.periodUnit && internal.periodUnit !== FhirPeriodUnit.Day) {
793
+ if (internal.periodUnit && internal.periodUnit !== types_1.FhirPeriodUnit.Day) {
261
794
  return;
262
795
  }
263
796
  if (internal.frequencyMax !== undefined || internal.periodMax !== undefined) {
264
797
  return;
265
798
  }
266
- const pairPreference = options.twoPerDayPair ?? "breakfast+dinner";
799
+ const pairPreference = (_a = options.twoPerDayPair) !== null && _a !== void 0 ? _a : "breakfast+dinner";
267
800
  const replacements = [];
268
- if (internal.when.includes(EventTiming["Before Meal"])) {
801
+ if ((0, array_1.arrayIncludes)(internal.when, types_1.EventTiming["Before Meal"])) {
269
802
  const specifics = computeMealExpansions("before", frequency, pairPreference);
270
803
  if (specifics) {
271
- replacements.push({ general: EventTiming["Before Meal"], specifics });
804
+ replacements.push({ general: types_1.EventTiming["Before Meal"], specifics });
272
805
  }
273
806
  }
274
- if (internal.when.includes(EventTiming["After Meal"])) {
807
+ if ((0, array_1.arrayIncludes)(internal.when, types_1.EventTiming["After Meal"])) {
275
808
  const specifics = computeMealExpansions("after", frequency, pairPreference);
276
809
  if (specifics) {
277
- replacements.push({ general: EventTiming["After Meal"], specifics });
810
+ replacements.push({ general: types_1.EventTiming["After Meal"], specifics });
278
811
  }
279
812
  }
280
- if (internal.when.includes(EventTiming.Meal)) {
813
+ if ((0, array_1.arrayIncludes)(internal.when, types_1.EventTiming.Meal)) {
281
814
  const specifics = computeMealExpansions("with", frequency, pairPreference);
282
815
  if (specifics) {
283
- replacements.push({ general: EventTiming.Meal, specifics });
816
+ replacements.push({ general: types_1.EventTiming.Meal, specifics });
284
817
  }
285
818
  }
286
819
  for (const { general, specifics } of replacements) {
@@ -292,15 +825,15 @@ function expandMealTimings(internal, options) {
292
825
  }
293
826
  function setRoute(internal, code, text) {
294
827
  internal.routeCode = code;
295
- internal.routeText = text ?? ROUTE_TEXT[code];
828
+ internal.routeText = text !== null && text !== void 0 ? text : maps_1.ROUTE_TEXT[code];
296
829
  }
297
830
  /**
298
831
  * Convert hour-based values into minutes when fractional quantities appear so
299
832
  * the resulting FHIR repeat payloads avoid unwieldy decimals.
300
833
  */
301
834
  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 };
835
+ if (unit === types_1.FhirPeriodUnit.Hour && (!Number.isInteger(value) || value < 1)) {
836
+ return { value: Math.round(value * 60 * 1000) / 1000, unit: types_1.FhirPeriodUnit.Minute };
304
837
  }
305
838
  return { value, unit };
306
839
  }
@@ -309,28 +842,28 @@ function normalizePeriodValue(value, unit) {
309
842
  * demand conversion into minutes.
310
843
  */
311
844
  function normalizePeriodRange(low, high, unit) {
312
- if (unit === FhirPeriodUnit.Hour && (!Number.isInteger(low) || !Number.isInteger(high) || low < 1 || high < 1)) {
845
+ if (unit === types_1.FhirPeriodUnit.Hour && (!Number.isInteger(low) || !Number.isInteger(high) || low < 1 || high < 1)) {
313
846
  return {
314
847
  low: Math.round(low * 60 * 1000) / 1000,
315
848
  high: Math.round(high * 60 * 1000) / 1000,
316
- unit: FhirPeriodUnit.Minute
849
+ unit: types_1.FhirPeriodUnit.Minute
317
850
  };
318
851
  }
319
852
  return { low, high, unit };
320
853
  }
321
854
  function periodUnitSuffix(unit) {
322
855
  switch (unit) {
323
- case FhirPeriodUnit.Minute:
856
+ case types_1.FhirPeriodUnit.Minute:
324
857
  return "min";
325
- case FhirPeriodUnit.Hour:
858
+ case types_1.FhirPeriodUnit.Hour:
326
859
  return "h";
327
- case FhirPeriodUnit.Day:
860
+ case types_1.FhirPeriodUnit.Day:
328
861
  return "d";
329
- case FhirPeriodUnit.Week:
862
+ case types_1.FhirPeriodUnit.Week:
330
863
  return "wk";
331
- case FhirPeriodUnit.Month:
864
+ case types_1.FhirPeriodUnit.Month:
332
865
  return "mo";
333
- case FhirPeriodUnit.Year:
866
+ case types_1.FhirPeriodUnit.Year:
334
867
  return "a";
335
868
  default:
336
869
  return undefined;
@@ -342,8 +875,8 @@ function maybeAssignTimingCode(internal, value, unit) {
342
875
  return;
343
876
  }
344
877
  const key = `q${value}${suffix}`;
345
- const descriptor = TIMING_ABBREVIATIONS[key];
346
- if (descriptor?.code && !internal.timingCode) {
878
+ const descriptor = maps_1.TIMING_ABBREVIATIONS[key];
879
+ if ((descriptor === null || descriptor === void 0 ? void 0 : descriptor.code) && !internal.timingCode) {
347
880
  internal.timingCode = descriptor.code;
348
881
  }
349
882
  }
@@ -352,18 +885,19 @@ function maybeAssignTimingCode(internal, value, unit) {
352
885
  * period clearly represents common cadences (daily/weekly/monthly).
353
886
  */
354
887
  function applyPeriod(internal, period, unit) {
888
+ var _a, _b, _c;
355
889
  const normalized = normalizePeriodValue(period, unit);
356
890
  internal.period = normalized.value;
357
891
  internal.periodUnit = normalized.unit;
358
892
  maybeAssignTimingCode(internal, normalized.value, normalized.unit);
359
- if (normalized.unit === FhirPeriodUnit.Day && normalized.value === 1) {
360
- internal.frequency = internal.frequency ?? 1;
893
+ if (normalized.unit === types_1.FhirPeriodUnit.Day && normalized.value === 1) {
894
+ internal.frequency = (_a = internal.frequency) !== null && _a !== void 0 ? _a : 1;
361
895
  }
362
- if (normalized.unit === FhirPeriodUnit.Week && normalized.value === 1) {
363
- internal.timingCode = internal.timingCode ?? "WK";
896
+ if (normalized.unit === types_1.FhirPeriodUnit.Week && normalized.value === 1) {
897
+ internal.timingCode = (_b = internal.timingCode) !== null && _b !== void 0 ? _b : "WK";
364
898
  }
365
- if (normalized.unit === FhirPeriodUnit.Month && normalized.value === 1) {
366
- internal.timingCode = internal.timingCode ?? "MO";
899
+ if (normalized.unit === types_1.FhirPeriodUnit.Month && normalized.value === 1) {
900
+ internal.timingCode = (_c = internal.timingCode) !== null && _c !== void 0 ? _c : "MO";
367
901
  }
368
902
  }
369
903
  /**
@@ -406,7 +940,7 @@ function tryParseCompactQ(internal, tokens, index) {
406
940
  }
407
941
  function applyFrequencyDescriptor(internal, token, descriptor, options) {
408
942
  if (descriptor.discouraged) {
409
- const check = checkDiscouraged(token.original, options);
943
+ const check = (0, safety_1.checkDiscouraged)(token.original, options);
410
944
  if (check.warning) {
411
945
  internal.warnings.push(check.warning);
412
946
  }
@@ -447,11 +981,11 @@ function parseMealContext(internal, tokens, index, code) {
447
981
  applyWhenToken(internal, token, code);
448
982
  return;
449
983
  }
450
- const meal = MEAL_KEYWORDS[next.lower];
984
+ const meal = maps_1.MEAL_KEYWORDS[next.lower];
451
985
  if (meal) {
452
- const whenCode = code === EventTiming["After Meal"]
986
+ const whenCode = code === types_1.EventTiming["After Meal"]
453
987
  ? meal.pc
454
- : code === EventTiming["Before Meal"]
988
+ : code === types_1.EventTiming["Before Meal"]
455
989
  ? meal.ac
456
990
  : code;
457
991
  applyWhenToken(internal, token, whenCode);
@@ -519,19 +1053,19 @@ function mapIntervalUnit(token) {
519
1053
  token === "minute" ||
520
1054
  token === "minutes" ||
521
1055
  token === "m") {
522
- return FhirPeriodUnit.Minute;
1056
+ return types_1.FhirPeriodUnit.Minute;
523
1057
  }
524
1058
  if (token === "h" || token === "hr" || token === "hrs" || token === "hour" || token === "hours") {
525
- return FhirPeriodUnit.Hour;
1059
+ return types_1.FhirPeriodUnit.Hour;
526
1060
  }
527
1061
  if (token === "d" || token === "day" || token === "days") {
528
- return FhirPeriodUnit.Day;
1062
+ return types_1.FhirPeriodUnit.Day;
529
1063
  }
530
1064
  if (token === "wk" || token === "w" || token === "week" || token === "weeks") {
531
- return FhirPeriodUnit.Week;
1065
+ return types_1.FhirPeriodUnit.Week;
532
1066
  }
533
1067
  if (token === "mo" || token === "month" || token === "months") {
534
- return FhirPeriodUnit.Month;
1068
+ return types_1.FhirPeriodUnit.Month;
535
1069
  }
536
1070
  return undefined;
537
1071
  }
@@ -547,7 +1081,8 @@ function parseNumericRange(token) {
547
1081
  }
548
1082
  return { low, high };
549
1083
  }
550
- export function parseInternal(input, options) {
1084
+ function parseInternal(input, options) {
1085
+ var _a, _b, _c, _d, _e, _f;
551
1086
  const tokens = tokenize(input);
552
1087
  const internal = {
553
1088
  input,
@@ -555,11 +1090,12 @@ export function parseInternal(input, options) {
555
1090
  consumed: new Set(),
556
1091
  dayOfWeek: [],
557
1092
  when: [],
558
- warnings: []
1093
+ warnings: [],
1094
+ siteTokenIndices: new Set()
559
1095
  };
560
- const context = options?.context ?? undefined;
561
- const customRouteMap = options?.routeMap
562
- ? new Map(Object.entries(options.routeMap).map(([key, value]) => [
1096
+ const context = (_a = options === null || options === void 0 ? void 0 : options.context) !== null && _a !== void 0 ? _a : undefined;
1097
+ const customRouteMap = (options === null || options === void 0 ? void 0 : options.routeMap)
1098
+ ? new Map((0, object_1.objectEntries)(options.routeMap).map(([key, value]) => [
563
1099
  key.toLowerCase(),
564
1100
  value
565
1101
  ]))
@@ -577,12 +1113,12 @@ export function parseInternal(input, options) {
577
1113
  prnReasonStart = i + 1;
578
1114
  break;
579
1115
  }
580
- if (token.lower === "as" && tokens[i + 1]?.lower === "needed") {
1116
+ if (token.lower === "as" && ((_b = tokens[i + 1]) === null || _b === void 0 ? void 0 : _b.lower) === "needed") {
581
1117
  internal.asNeeded = true;
582
1118
  mark(internal.consumed, token);
583
1119
  mark(internal.consumed, tokens[i + 1]);
584
1120
  let reasonIndex = i + 2;
585
- if (tokens[reasonIndex]?.lower === "for") {
1121
+ if (((_c = tokens[reasonIndex]) === null || _c === void 0 ? void 0 : _c.lower) === "for") {
586
1122
  mark(internal.consumed, tokens[reasonIndex]);
587
1123
  reasonIndex += 1;
588
1124
  }
@@ -603,7 +1139,7 @@ export function parseInternal(input, options) {
603
1139
  }
604
1140
  internal.frequency = freq;
605
1141
  internal.period = 1;
606
- internal.periodUnit = FhirPeriodUnit.Day;
1142
+ internal.periodUnit = types_1.FhirPeriodUnit.Day;
607
1143
  mark(internal.consumed, token);
608
1144
  }
609
1145
  }
@@ -616,14 +1152,17 @@ export function parseInternal(input, options) {
616
1152
  continue;
617
1153
  }
618
1154
  const phrase = slice.map((part) => part.lower).join(" ");
619
- const customCode = customRouteMap?.get(phrase);
1155
+ const customCode = customRouteMap === null || customRouteMap === void 0 ? void 0 : customRouteMap.get(phrase);
620
1156
  const synonym = customCode
621
- ? { code: customCode, text: ROUTE_TEXT[customCode] }
622
- : DEFAULT_ROUTE_SYNONYMS[phrase];
1157
+ ? { code: customCode, text: maps_1.ROUTE_TEXT[customCode] }
1158
+ : maps_1.DEFAULT_ROUTE_SYNONYMS[phrase];
623
1159
  if (synonym) {
624
1160
  setRoute(internal, synonym.code, synonym.text);
625
1161
  for (const part of slice) {
626
1162
  mark(internal.consumed, part);
1163
+ if (BODY_SITE_HINTS.has(part.lower)) {
1164
+ internal.siteTokenIndices.add(part.index);
1165
+ }
627
1166
  }
628
1167
  return true;
629
1168
  }
@@ -635,12 +1174,13 @@ export function parseInternal(input, options) {
635
1174
  if (internal.consumed.has(token.index)) {
636
1175
  continue;
637
1176
  }
1177
+ const normalizedLower = normalizeTokenLower(token);
638
1178
  if (token.lower === "bld" || token.lower === "b-l-d") {
639
- const check = checkDiscouraged(token.original, options);
1179
+ const check = (0, safety_1.checkDiscouraged)(token.original, options);
640
1180
  if (check.warning) {
641
1181
  internal.warnings.push(check.warning);
642
1182
  }
643
- applyWhenToken(internal, token, EventTiming.Meal);
1183
+ applyWhenToken(internal, token, types_1.EventTiming.Meal);
644
1184
  continue;
645
1185
  }
646
1186
  if (token.lower === "q") {
@@ -648,8 +1188,25 @@ export function parseInternal(input, options) {
648
1188
  continue;
649
1189
  }
650
1190
  }
1191
+ if (tryParseNumericCadence(internal, tokens, i)) {
1192
+ continue;
1193
+ }
1194
+ const eyeSite = EYE_SITE_TOKENS[normalizedLower];
1195
+ const treatEyeTokenAsSite = eyeSite
1196
+ ? shouldTreatEyeTokenAsSite(internal, tokens, i, context)
1197
+ : false;
1198
+ if (normalizedLower === "od") {
1199
+ const descriptor = maps_1.TIMING_ABBREVIATIONS.od;
1200
+ if (descriptor &&
1201
+ shouldInterpretOdAsOnceDaily(internal, tokens, i, treatEyeTokenAsSite)) {
1202
+ applyFrequencyDescriptor(internal, token, descriptor, options);
1203
+ continue;
1204
+ }
1205
+ }
651
1206
  // Frequency abbreviation map
652
- const freqDescriptor = TIMING_ABBREVIATIONS[token.lower];
1207
+ const freqDescriptor = normalizedLower === "od"
1208
+ ? undefined
1209
+ : (_d = maps_1.TIMING_ABBREVIATIONS[token.lower]) !== null && _d !== void 0 ? _d : maps_1.TIMING_ABBREVIATIONS[normalizedLower];
653
1210
  if (freqDescriptor) {
654
1211
  applyFrequencyDescriptor(internal, token, freqDescriptor, options);
655
1212
  continue;
@@ -660,34 +1217,34 @@ export function parseInternal(input, options) {
660
1217
  // Event timing tokens
661
1218
  if (token.lower === "pc" || token.lower === "ac") {
662
1219
  parseMealContext(internal, tokens, i, token.lower === "pc"
663
- ? EventTiming["After Meal"]
664
- : EventTiming["Before Meal"]);
1220
+ ? types_1.EventTiming["After Meal"]
1221
+ : types_1.EventTiming["Before Meal"]);
665
1222
  continue;
666
1223
  }
667
1224
  const nextToken = tokens[i + 1];
668
1225
  if (nextToken && !internal.consumed.has(nextToken.index)) {
669
1226
  const combo = `${token.lower} ${nextToken.lower}`;
670
- const comboWhen = COMBO_EVENT_TIMINGS[combo] ?? EVENT_TIMING_TOKENS[combo];
1227
+ const comboWhen = (_e = COMBO_EVENT_TIMINGS[combo]) !== null && _e !== void 0 ? _e : maps_1.EVENT_TIMING_TOKENS[combo];
671
1228
  if (comboWhen) {
672
1229
  applyWhenToken(internal, token, comboWhen);
673
1230
  mark(internal.consumed, nextToken);
674
1231
  continue;
675
1232
  }
676
1233
  }
677
- const customWhen = options?.whenMap?.[token.lower];
1234
+ const customWhen = (_f = options === null || options === void 0 ? void 0 : options.whenMap) === null || _f === void 0 ? void 0 : _f[token.lower];
678
1235
  if (customWhen) {
679
1236
  applyWhenToken(internal, token, customWhen);
680
1237
  continue;
681
1238
  }
682
- const whenCode = EVENT_TIMING_TOKENS[token.lower];
1239
+ const whenCode = maps_1.EVENT_TIMING_TOKENS[token.lower];
683
1240
  if (whenCode) {
684
1241
  applyWhenToken(internal, token, whenCode);
685
1242
  continue;
686
1243
  }
687
1244
  // Day of week
688
- const day = DAY_OF_WEEK_TOKENS[token.lower];
1245
+ const day = maps_1.DAY_OF_WEEK_TOKENS[token.lower];
689
1246
  if (day) {
690
- if (!internal.dayOfWeek.includes(day)) {
1247
+ if (!(0, array_1.arrayIncludes)(internal.dayOfWeek, day)) {
691
1248
  internal.dayOfWeek.push(day);
692
1249
  }
693
1250
  mark(internal.consumed, token);
@@ -697,9 +1254,9 @@ export function parseInternal(input, options) {
697
1254
  if (tryRouteSynonym(i)) {
698
1255
  continue;
699
1256
  }
700
- const eyeSite = EYE_SITE_TOKENS[token.lower];
701
- if (eyeSite) {
1257
+ if (eyeSite && treatEyeTokenAsSite) {
702
1258
  internal.siteText = eyeSite.site;
1259
+ internal.siteSource = "abbreviation";
703
1260
  if (eyeSite.route && !internal.routeCode) {
704
1261
  setRoute(internal, eyeSite.route);
705
1262
  }
@@ -750,7 +1307,7 @@ export function parseInternal(input, options) {
750
1307
  continue;
751
1308
  }
752
1309
  // Words for frequency
753
- const wordFreq = WORD_FREQUENCIES[token.lower];
1310
+ const wordFreq = maps_1.WORD_FREQUENCIES[token.lower];
754
1311
  if (wordFreq) {
755
1312
  internal.frequency = wordFreq.frequency;
756
1313
  internal.period = 1;
@@ -759,7 +1316,7 @@ export function parseInternal(input, options) {
759
1316
  continue;
760
1317
  }
761
1318
  // Skip generic connectors
762
- if (token.lower === "per" || token.lower === "a" || token.lower === "every") {
1319
+ if (token.lower === "per" || token.lower === "a" || token.lower === "every" || token.lower === "each") {
763
1320
  mark(internal.consumed, token);
764
1321
  continue;
765
1322
  }
@@ -778,13 +1335,19 @@ export function parseInternal(input, options) {
778
1335
  }
779
1336
  }
780
1337
  if (internal.unit === undefined) {
781
- internal.unit = inferUnitFromContext(context);
1338
+ internal.unit = enforceHouseholdUnitPolicy((0, context_1.inferUnitFromContext)(context), options);
1339
+ }
1340
+ if (internal.unit === undefined) {
1341
+ const fallbackUnit = enforceHouseholdUnitPolicy(inferUnitFromRouteHints(internal), options);
1342
+ if (fallbackUnit) {
1343
+ internal.unit = fallbackUnit;
1344
+ }
782
1345
  }
783
1346
  // Frequency defaults when timing code implies it
784
1347
  if (internal.frequency === undefined &&
785
1348
  internal.period === undefined &&
786
1349
  internal.timingCode) {
787
- const descriptor = TIMING_ABBREVIATIONS[internal.timingCode.toLowerCase()];
1350
+ const descriptor = maps_1.TIMING_ABBREVIATIONS[internal.timingCode.toLowerCase()];
788
1351
  if (descriptor) {
789
1352
  if (descriptor.frequency !== undefined) {
790
1353
  internal.frequency = descriptor.frequency;
@@ -804,7 +1367,7 @@ export function parseInternal(input, options) {
804
1367
  }
805
1368
  if (!internal.timingCode &&
806
1369
  internal.frequency !== undefined &&
807
- internal.periodUnit === FhirPeriodUnit.Day &&
1370
+ internal.periodUnit === types_1.FhirPeriodUnit.Day &&
808
1371
  (internal.period === undefined || internal.period === 1)) {
809
1372
  if (internal.frequency === 2) {
810
1373
  internal.timingCode = "BID";
@@ -816,23 +1379,82 @@ export function parseInternal(input, options) {
816
1379
  internal.timingCode = "QID";
817
1380
  }
818
1381
  }
1382
+ reconcileMealTimingSpecificity(internal);
819
1383
  // Expand generic meal markers into specific EventTiming codes when asked to.
820
1384
  expandMealTimings(internal, options);
1385
+ sortWhenValues(internal, options);
821
1386
  // Determine site text from leftover tokens (excluding PRN reason tokens)
822
1387
  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);
1388
+ const siteCandidateIndices = new Set();
1389
+ for (const token of leftoverTokens) {
1390
+ if (BODY_SITE_HINTS.has(token.lower)) {
1391
+ siteCandidateIndices.add(token.index);
1392
+ }
1393
+ }
1394
+ for (const idx of internal.siteTokenIndices) {
1395
+ siteCandidateIndices.add(idx);
1396
+ }
1397
+ if (siteCandidateIndices.size > 0) {
1398
+ const indicesToInclude = new Set(siteCandidateIndices);
1399
+ for (const idx of siteCandidateIndices) {
1400
+ let prev = idx - 1;
1401
+ while (prev >= 0) {
1402
+ const token = tokens[prev];
1403
+ if (!token) {
1404
+ break;
832
1405
  }
1406
+ const lower = token.lower;
1407
+ if (SITE_CONNECTORS.has(lower) || BODY_SITE_HINTS.has(lower)) {
1408
+ indicesToInclude.add(token.index);
1409
+ prev -= 1;
1410
+ continue;
1411
+ }
1412
+ break;
1413
+ }
1414
+ let next = idx + 1;
1415
+ while (next < tokens.length) {
1416
+ const token = tokens[next];
1417
+ if (!token) {
1418
+ break;
1419
+ }
1420
+ const lower = token.lower;
1421
+ if (SITE_CONNECTORS.has(lower) || BODY_SITE_HINTS.has(lower)) {
1422
+ indicesToInclude.add(token.index);
1423
+ next += 1;
1424
+ continue;
1425
+ }
1426
+ break;
1427
+ }
1428
+ }
1429
+ const sortedIndices = Array.from(indicesToInclude).sort((a, b) => a - b);
1430
+ const displayWords = [];
1431
+ for (const index of sortedIndices) {
1432
+ const token = tokens[index];
1433
+ if (!token) {
1434
+ continue;
833
1435
  }
834
- if (words.length > 0) {
835
- internal.siteText = words.join(" ");
1436
+ const lower = token.lower;
1437
+ if (!SITE_CONNECTORS.has(lower) && !SITE_FILLER_WORDS.has(lower)) {
1438
+ displayWords.push(token.original);
1439
+ }
1440
+ mark(internal.consumed, token);
1441
+ }
1442
+ const normalizedSite = displayWords
1443
+ .filter((word) => !SITE_CONNECTORS.has(word.trim().toLowerCase()))
1444
+ .join(" ")
1445
+ .trim();
1446
+ if (normalizedSite) {
1447
+ internal.siteText = normalizedSite;
1448
+ if (!internal.siteSource) {
1449
+ internal.siteSource = "text";
1450
+ }
1451
+ }
1452
+ }
1453
+ if (!internal.routeCode && internal.siteText) {
1454
+ for (const { pattern, route } of SITE_UNIT_ROUTE_HINTS) {
1455
+ if (pattern.test(internal.siteText)) {
1456
+ setRoute(internal, route);
1457
+ break;
836
1458
  }
837
1459
  }
838
1460
  }
@@ -851,20 +1473,65 @@ export function parseInternal(input, options) {
851
1473
  internal.asNeededReason = reasonTokens.join(" ");
852
1474
  }
853
1475
  }
854
- if (internal.routeCode === RouteCode["Intravitreal route (qualifier value)"] &&
1476
+ if (internal.routeCode === types_1.RouteCode["Intravitreal route (qualifier value)"] &&
855
1477
  (!internal.siteText || !/eye/i.test(internal.siteText))) {
856
1478
  internal.warnings.push("Intravitreal administrations require an eye site (e.g., OD/OS/OU).");
857
1479
  }
858
1480
  return internal;
859
1481
  }
860
1482
  function normalizeUnit(token, options) {
861
- const override = options?.unitMap?.[token];
1483
+ var _a;
1484
+ const override = enforceHouseholdUnitPolicy((_a = options === null || options === void 0 ? void 0 : options.unitMap) === null || _a === void 0 ? void 0 : _a[token], options);
862
1485
  if (override) {
863
1486
  return override;
864
1487
  }
865
- const defaultUnit = DEFAULT_UNIT_SYNONYMS[token];
1488
+ const defaultUnit = enforceHouseholdUnitPolicy(maps_1.DEFAULT_UNIT_SYNONYMS[token], options);
866
1489
  if (defaultUnit) {
867
1490
  return defaultUnit;
868
1491
  }
869
1492
  return undefined;
870
1493
  }
1494
+ function enforceHouseholdUnitPolicy(unit, options) {
1495
+ if (unit &&
1496
+ (options === null || options === void 0 ? void 0 : options.allowHouseholdVolumeUnits) === false &&
1497
+ HOUSEHOLD_VOLUME_UNIT_SET.has(unit.toLowerCase())) {
1498
+ return undefined;
1499
+ }
1500
+ return unit;
1501
+ }
1502
+ function inferUnitFromRouteHints(internal) {
1503
+ if (internal.routeCode) {
1504
+ const unit = maps_1.DEFAULT_UNIT_BY_ROUTE[internal.routeCode];
1505
+ if (unit) {
1506
+ return unit;
1507
+ }
1508
+ }
1509
+ if (internal.routeText) {
1510
+ const normalized = internal.routeText.trim().toLowerCase();
1511
+ const synonym = maps_1.DEFAULT_ROUTE_SYNONYMS[normalized];
1512
+ if (synonym) {
1513
+ const unit = maps_1.DEFAULT_UNIT_BY_ROUTE[synonym.code];
1514
+ if (unit) {
1515
+ return unit;
1516
+ }
1517
+ }
1518
+ }
1519
+ if (internal.siteText) {
1520
+ const unit = inferUnitFromSiteText(internal.siteText);
1521
+ if (unit) {
1522
+ return unit;
1523
+ }
1524
+ }
1525
+ return undefined;
1526
+ }
1527
+ function inferUnitFromSiteText(siteText) {
1528
+ for (const { pattern, route } of SITE_UNIT_ROUTE_HINTS) {
1529
+ if (pattern.test(siteText)) {
1530
+ const unit = maps_1.DEFAULT_UNIT_BY_ROUTE[route];
1531
+ if (unit) {
1532
+ return unit;
1533
+ }
1534
+ }
1535
+ }
1536
+ return undefined;
1537
+ }