ezmedicationinput 0.1.8 → 0.1.11

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/README.md CHANGED
@@ -14,6 +14,7 @@
14
14
  - Applies medication context to infer default units when they are omitted.
15
15
  - Surfaces warnings when discouraged tokens (`QD`, `QOD`, `BLD`) are used and optionally rejects them.
16
16
  - Generates upcoming administration timestamps from FHIR dosage data via `nextDueDoses` using configurable clinic clocks.
17
+ - Auto-codes common body-site phrases (e.g. "left arm", "right eye") with SNOMED CT anatomy concepts and supports interactive lookup flows for ambiguous sites.
17
18
 
18
19
  ## Installation
19
20
 
@@ -110,6 +111,105 @@ When `when` is populated, `timeOfDay` is intentionally omitted to stay within HL
110
111
 
111
112
  Routes always include SNOMED CT codings. Every code from the SNOMED Route of Administration value set is represented so you can confidently pass parsed results into downstream FHIR services that expect coded routes.
112
113
 
114
+ ### SNOMED body-site coding & interactive probes
115
+
116
+ Spelled-out application sites are automatically coded when the phrase is known to the bundled SNOMED CT anatomy dictionary. The normalized site text is also surfaced in `Dosage.site.text` and in the `ParseResult.meta.normalized.site` object.
117
+
118
+ ```ts
119
+ import { parseSig } from "ezmedicationinput";
120
+
121
+ const result = parseSig("apply cream to left arm twice daily");
122
+
123
+ result.fhir.site?.coding?.[0];
124
+ // → { system: "http://snomed.info/sct", code: "368208006", display: "Left upper arm structure" }
125
+ ```
126
+
127
+ When the parser encounters an unfamiliar site, it leaves the text untouched and records nothing in `meta.siteLookups`. Wrapping the phrase in braces (e.g. `apply to {mole on scalp}`) preserves the same parsing behavior but flags the entry as a **probe** so `meta.siteLookups` always contains the request. This allows UIs to display lookup widgets even before a matching code exists. Braces are optional when the site is already recognized—they simply make the clinician's intent explicit.
128
+
129
+ You can extend or replace the built-in codings via `ParseOptions`:
130
+
131
+ ```ts
132
+ import { parseSigAsync } from "ezmedicationinput";
133
+
134
+ const result = await parseSigAsync("apply to {left temple} nightly", {
135
+ siteCodeMap: {
136
+ "left temple": {
137
+ coding: {
138
+ system: "http://example.org/custom",
139
+ code: "LTEMP",
140
+ display: "Left temple"
141
+ }
142
+ }
143
+ },
144
+ siteCodeResolvers: async (request) => {
145
+ if (request.canonical === "mole on scalp") {
146
+ return {
147
+ coding: { system: "http://snomed.info/sct", code: "39937001", display: "Scalp structure" },
148
+ text: request.text
149
+ };
150
+ }
151
+ return undefined;
152
+ },
153
+ siteCodeSuggestionResolvers: async (request) => {
154
+ if (request.isProbe) {
155
+ return [
156
+ {
157
+ coding: { system: "http://snomed.info/sct", code: "39937001", display: "Scalp structure" },
158
+ text: "Scalp"
159
+ },
160
+ {
161
+ coding: { system: "http://snomed.info/sct", code: "280447003", display: "Temple region of head" },
162
+ text: "Temple"
163
+ }
164
+ ];
165
+ }
166
+ return undefined;
167
+ }
168
+ });
169
+
170
+ result.meta.siteLookups;
171
+ // → [{ request: { text: "left temple", isProbe: true, ... }, suggestions: [...] }]
172
+ ```
173
+
174
+ - `siteCodeMap` lets you supply deterministic overrides for normalized site phrases.
175
+ - `siteCodeResolvers` (sync or async) can call external services to resolve sites on demand.
176
+ - `siteCodeSuggestionResolvers` return candidate codes; their results populate `meta.siteLookups[0].suggestions`.
177
+ - Each resolver receives the full `SiteCodeLookupRequest`, including the original input, the cleaned site text, and a `{ start, end }` range you can use to highlight the substring in UI workflows.
178
+ - `parseSigAsync` behaves like `parseSig` but awaits asynchronous resolvers and suggestion providers.
179
+
180
+ #### Site resolver signatures
181
+
182
+ ```ts
183
+ export interface SiteCodeLookupRequest {
184
+ originalText: string; // Sanitized phrase before brace/whitespace cleanup
185
+ text: string; // Brace-free, whitespace-collapsed site text
186
+ normalized: string; // Lower-case variant of `text`
187
+ canonical: string; // Normalized key for dictionary lookups
188
+ isProbe: boolean; // True when the sig used `{placeholder}` syntax
189
+ inputText: string; // Full sig string the parser received
190
+ sourceText?: string; // Substring extracted from `inputText`
191
+ range?: { start: number; end: number }; // Character range of `sourceText`
192
+ }
193
+
194
+ export type SiteCodeResolver = (
195
+ request: SiteCodeLookupRequest
196
+ ) => SiteCodeResolution | null | undefined | Promise<SiteCodeResolution | null | undefined>;
197
+
198
+ export type SiteCodeSuggestionResolver = (
199
+ request: SiteCodeLookupRequest
200
+ ) =>
201
+ | SiteCodeSuggestionsResult
202
+ | SiteCodeSuggestion[]
203
+ | SiteCodeSuggestion
204
+ | null
205
+ | undefined
206
+ | Promise<SiteCodeSuggestionsResult | SiteCodeSuggestion[] | SiteCodeSuggestion | null | undefined>;
207
+ ```
208
+
209
+ `SiteCodeResolution`, `SiteCodeSuggestion`, and `SiteCodeSuggestionsResult` mirror the values shown in the example above. Resolvers can use `request.range` (start inclusive, end exclusive) together with `request.sourceText` to paint highlights or replace the detected phrase in client applications.
210
+
211
+ Consumers that only need synchronous resolution can continue calling `parseSig`. If any synchronous resolver accidentally returns a Promise, an error is thrown with guidance to switch to `parseSigAsync`.
212
+
113
213
  You can specify the number of times (total count) the medication is supposed to be used by ending with `for {number} times`, `x {number} doses`, or simply `x {number}`
114
214
 
115
215
  ### Advanced parsing options
package/dist/fhir.js CHANGED
@@ -9,7 +9,7 @@ const object_1 = require("./utils/object");
9
9
  const array_1 = require("./utils/array");
10
10
  const SNOMED_SYSTEM = "http://snomed.info/sct";
11
11
  function toFhir(internal) {
12
- var _a, _b;
12
+ var _a, _b, _c, _d, _e;
13
13
  const dosage = {};
14
14
  const repeat = {};
15
15
  let hasRepeat = false;
@@ -100,8 +100,20 @@ function toFhir(internal) {
100
100
  dosage.route = { text };
101
101
  }
102
102
  }
103
- if (internal.siteText) {
104
- dosage.site = { text: internal.siteText };
103
+ if (internal.siteText || ((_c = internal.siteCoding) === null || _c === void 0 ? void 0 : _c.code)) {
104
+ const coding = ((_d = internal.siteCoding) === null || _d === void 0 ? void 0 : _d.code)
105
+ ? [
106
+ {
107
+ system: (_e = internal.siteCoding.system) !== null && _e !== void 0 ? _e : SNOMED_SYSTEM,
108
+ code: internal.siteCoding.code,
109
+ display: internal.siteCoding.display
110
+ }
111
+ ]
112
+ : undefined;
113
+ dosage.site = {
114
+ text: internal.siteText,
115
+ coding
116
+ };
105
117
  }
106
118
  if (internal.asNeeded) {
107
119
  dosage.asNeededBoolean = true;
@@ -116,7 +128,7 @@ function toFhir(internal) {
116
128
  return dosage;
117
129
  }
118
130
  function internalFromFhir(dosage) {
119
- var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m, _o, _p, _q, _r, _s, _t, _u, _v, _w, _x, _y, _z, _0, _1, _2, _3, _4, _5;
131
+ var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m, _o, _p, _q, _r, _s, _t, _u, _v, _w, _x, _y, _z, _0, _1, _2, _3, _4, _5, _6, _7;
120
132
  const internal = {
121
133
  input: (_a = dosage.text) !== null && _a !== void 0 ? _a : "",
122
134
  tokens: [],
@@ -139,7 +151,8 @@ function internalFromFhir(dosage) {
139
151
  siteText: (_y = dosage.site) === null || _y === void 0 ? void 0 : _y.text,
140
152
  asNeeded: dosage.asNeededBoolean,
141
153
  asNeededReason: (_0 = (_z = dosage.asNeededFor) === null || _z === void 0 ? void 0 : _z[0]) === null || _0 === void 0 ? void 0 : _0.text,
142
- siteTokenIndices: new Set()
154
+ siteTokenIndices: new Set(),
155
+ siteLookups: []
143
156
  };
144
157
  const routeCoding = (_2 = (_1 = dosage.route) === null || _1 === void 0 ? void 0 : _1.coding) === null || _2 === void 0 ? void 0 : _2.find((code) => code.system === SNOMED_SYSTEM);
145
158
  if (routeCoding === null || routeCoding === void 0 ? void 0 : routeCoding.code) {
@@ -150,13 +163,21 @@ function internalFromFhir(dosage) {
150
163
  internal.routeText = maps_1.ROUTE_TEXT[mapped];
151
164
  }
152
165
  }
153
- const doseAndRate = (_3 = dosage.doseAndRate) === null || _3 === void 0 ? void 0 : _3[0];
166
+ const siteCoding = (_4 = (_3 = dosage.site) === null || _3 === void 0 ? void 0 : _3.coding) === null || _4 === void 0 ? void 0 : _4.find((code) => code.system === SNOMED_SYSTEM);
167
+ if (siteCoding === null || siteCoding === void 0 ? void 0 : siteCoding.code) {
168
+ internal.siteCoding = {
169
+ code: siteCoding.code,
170
+ display: siteCoding.display,
171
+ system: siteCoding.system
172
+ };
173
+ }
174
+ const doseAndRate = (_5 = dosage.doseAndRate) === null || _5 === void 0 ? void 0 : _5[0];
154
175
  if (doseAndRate === null || doseAndRate === void 0 ? void 0 : doseAndRate.doseRange) {
155
176
  const { low, high } = doseAndRate.doseRange;
156
177
  if ((low === null || low === void 0 ? void 0 : low.value) !== undefined && (high === null || high === void 0 ? void 0 : high.value) !== undefined) {
157
178
  internal.doseRange = { low: low.value, high: high.value };
158
179
  }
159
- internal.unit = (_5 = (_4 = low === null || low === void 0 ? void 0 : low.unit) !== null && _4 !== void 0 ? _4 : high === null || high === void 0 ? void 0 : high.unit) !== null && _5 !== void 0 ? _5 : internal.unit;
180
+ internal.unit = (_7 = (_6 = low === null || low === void 0 ? void 0 : low.unit) !== null && _6 !== void 0 ? _6 : high === null || high === void 0 ? void 0 : high.unit) !== null && _7 !== void 0 ? _7 : internal.unit;
160
181
  }
161
182
  else if (doseAndRate === null || doseAndRate === void 0 ? void 0 : doseAndRate.doseQuantity) {
162
183
  const dose = doseAndRate.doseQuantity;
package/dist/i18n.js CHANGED
@@ -126,39 +126,103 @@ const DAY_NAMES_THAI = {
126
126
  sun: "วันอาทิตย์"
127
127
  };
128
128
  exports.THAI_SITE_TRANSLATIONS = {
129
+ eye: "ตา",
130
+ eyes: "ตา",
129
131
  "right eye": "ตาขวา",
130
132
  "left eye": "ตาซ้าย",
131
133
  "both eyes": "ตาทั้งสองข้าง",
134
+ "bilateral eyes": "ตาทั้งสองข้าง",
132
135
  "right ear": "หูขวา",
133
136
  "left ear": "หูซ้าย",
134
137
  "both ears": "หูทั้งสองข้าง",
138
+ "bilateral ears": "หูทั้งสองข้าง",
135
139
  ear: "หู",
136
140
  ears: "หูทั้งสองข้าง",
141
+ nostril: "รูจมูก",
142
+ nostrils: "รูจมูกทั้งสองข้าง",
137
143
  "right nostril": "รูจมูกขวา",
138
144
  "left nostril": "รูจมูกซ้าย",
139
145
  "both nostrils": "รูจมูกทั้งสองข้าง",
146
+ "left naris": "รูจมูกซ้าย",
147
+ "right naris": "รูจมูกขวา",
148
+ nares: "รูจมูกทั้งสองข้าง",
149
+ "anterior nares": "รูจมูกด้านหน้า",
150
+ nose: "จมูก",
151
+ mouth: "ปาก",
152
+ tongue: "ลิ้น",
153
+ tongues: "ลิ้น",
140
154
  "right arm": "แขนขวา",
141
155
  "left arm": "แขนซ้าย",
142
156
  "both arms": "แขนทั้งสองข้าง",
157
+ arm: "แขน",
158
+ "upper arm": "ต้นแขน",
159
+ "left upper arm": "ต้นแขนซ้าย",
160
+ "right upper arm": "ต้นแขนขวา",
161
+ "bilateral arms": "แขนทั้งสองข้าง",
143
162
  "right leg": "ขาขวา",
144
163
  "left leg": "ขาซ้าย",
145
164
  "both legs": "ขาทั้งสองข้าง",
165
+ leg: "ขา",
166
+ "lower leg": "ขาส่วนล่าง",
167
+ "left lower leg": "ขาส่วนล่างซ้าย",
168
+ "right lower leg": "ขาส่วนล่างขวา",
169
+ "bilateral legs": "ขาทั้งสองข้าง",
146
170
  "right hand": "มือขวา",
147
171
  "left hand": "มือซ้าย",
148
172
  "both hands": "มือทั้งสองข้าง",
173
+ hand: "มือ",
174
+ hands: "มือทั้งสองข้าง",
149
175
  "right foot": "เท้าขวา",
150
176
  "left foot": "เท้าซ้าย",
151
177
  "both feet": "เท้าทั้งสองข้าง",
178
+ foot: "เท้า",
179
+ feet: "เท้า",
152
180
  abdomen: "ช่องท้อง",
181
+ abdominal: "ช่องท้อง",
153
182
  belly: "ท้อง",
154
183
  back: "แผ่นหลัง",
184
+ scalp: "หนังศีรษะ",
185
+ face: "ใบหน้า",
155
186
  cheek: "แก้ม",
156
187
  cheeks: "แก้มทั้งสองข้าง",
188
+ forehead: "หน้าผาก",
189
+ chin: "คาง",
190
+ neck: "คอ",
157
191
  forearm: "ปลายแขน",
192
+ "left forearm": "ปลายแขนซ้าย",
193
+ "right forearm": "ปลายแขนขวา",
158
194
  shoulder: "ไหล่",
159
195
  shoulders: "ไหล่ทั้งสองข้าง",
160
196
  thigh: "ต้นขา",
161
- thighs: "ต้นขาทั้งสองข้าง"
197
+ thighs: "ต้นขาทั้งสองข้าง",
198
+ "left thigh": "ต้นขาซ้าย",
199
+ "right thigh": "ต้นขาขวา",
200
+ gum: "เหงือก",
201
+ gums: "เหงือก",
202
+ tooth: "ฟัน",
203
+ teeth: "ฟัน",
204
+ buttock: "สะโพก",
205
+ buttocks: "สะโพกทั้งสองข้าง",
206
+ gluteal: "สะโพก",
207
+ glute: "สะโพก",
208
+ "left buttock": "สะโพกซ้าย",
209
+ "left gluteal": "สะโพกซ้าย",
210
+ "right buttock": "สะโพกขวา",
211
+ "right gluteal": "สะโพกขวา",
212
+ muscle: "กล้ามเนื้อ",
213
+ muscles: "กล้ามเนื้อทั้งหมด",
214
+ vein: "หลอดเลือดดำ",
215
+ veins: "หลอดเลือดดำทั้งหมด",
216
+ vagina: "ช่องคลอด",
217
+ vaginal: "บริเวณช่องคลอด",
218
+ penis: "อวัยวะเพศชาย",
219
+ penile: "บริเวณอวัยวะเพศชาย",
220
+ rectum: "ไส้ตรง",
221
+ rectal: "บริเวณทวารหนัก",
222
+ anus: "ทวารหนัก",
223
+ perineum: "ฝีเย็บ",
224
+ skin: "ผิวหนัง",
225
+ hair: "เส้นผม"
162
226
  };
163
227
  const DEFAULT_THAI_ROUTE_GRAMMAR = { verb: "ใช้" };
164
228
  const THAI_ROUTE_GRAMMAR = {
@@ -613,6 +677,9 @@ function formatShortThai(internal) {
613
677
  .join(",");
614
678
  parts.push(days);
615
679
  }
680
+ if (internal.count !== undefined) {
681
+ parts.push(`x${stripTrailingZero(internal.count)}`);
682
+ }
616
683
  const asNeeded = formatAsNeededThai(internal);
617
684
  if (asNeeded) {
618
685
  parts.push(asNeeded);
@@ -629,6 +696,9 @@ function formatLongThai(internal) {
629
696
  const eventParts = collectWhenPhrasesThai(internal);
630
697
  const timing = combineFrequencyAndEventsThai(frequencyPart, eventParts);
631
698
  const dayPart = describeDayOfWeekThai(internal);
699
+ const countPart = internal.count !== undefined
700
+ ? `จำนวน ${stripTrailingZero(internal.count)} ครั้ง`
701
+ : undefined;
632
702
  const asNeeded = formatAsNeededThai(internal);
633
703
  const segments = [dosePart];
634
704
  if (routePart) {
@@ -643,6 +713,9 @@ function formatLongThai(internal) {
643
713
  if (dayPart) {
644
714
  segments.push(dayPart);
645
715
  }
716
+ if (countPart) {
717
+ segments.push(countPart);
718
+ }
646
719
  if (asNeeded) {
647
720
  segments.push(asNeeded);
648
721
  }
package/dist/index.d.ts CHANGED
@@ -6,5 +6,6 @@ export { nextDueDoses } from "./schedule";
6
6
  export { getRegisteredSigLocalizations, registerSigLocalization, resolveSigLocalization, resolveSigTranslation } from "./i18n";
7
7
  export type { SigLocalization, SigLocalizationConfig, SigTranslation, SigTranslationConfig } from "./i18n";
8
8
  export declare function parseSig(input: string, options?: ParseOptions): ParseResult;
9
+ export declare function parseSigAsync(input: string, options?: ParseOptions): Promise<ParseResult>;
9
10
  export declare function formatSig(dosage: FhirDosage, style?: "short" | "long", options?: FormatOptions): string;
10
11
  export declare function fromFhirDosage(dosage: FhirDosage, options?: FormatOptions): ParseResult;
package/dist/index.js CHANGED
@@ -13,9 +13,19 @@ var __createBinding = (this && this.__createBinding) || (Object.create ? (functi
13
13
  var __exportStar = (this && this.__exportStar) || function(m, exports) {
14
14
  for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
15
15
  };
16
+ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
17
+ function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
18
+ return new (P || (P = Promise))(function (resolve, reject) {
19
+ function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
20
+ function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
21
+ function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
22
+ step((generator = generator.apply(thisArg, _arguments || [])).next());
23
+ });
24
+ };
16
25
  Object.defineProperty(exports, "__esModule", { value: true });
17
26
  exports.resolveSigTranslation = exports.resolveSigLocalization = exports.registerSigLocalization = exports.getRegisteredSigLocalizations = exports.nextDueDoses = exports.suggestSig = exports.parseInternal = void 0;
18
27
  exports.parseSig = parseSig;
28
+ exports.parseSigAsync = parseSigAsync;
19
29
  exports.formatSig = formatSig;
20
30
  exports.fromFhirDosage = fromFhirDosage;
21
31
  const format_1 = require("./format");
@@ -36,6 +46,56 @@ Object.defineProperty(exports, "resolveSigLocalization", { enumerable: true, get
36
46
  Object.defineProperty(exports, "resolveSigTranslation", { enumerable: true, get: function () { return i18n_2.resolveSigTranslation; } });
37
47
  function parseSig(input, options) {
38
48
  const internal = (0, parser_1.parseInternal)(input, options);
49
+ (0, parser_1.applySiteCoding)(internal, options);
50
+ return buildParseResult(internal, options);
51
+ }
52
+ function parseSigAsync(input, options) {
53
+ return __awaiter(this, void 0, void 0, function* () {
54
+ const internal = (0, parser_1.parseInternal)(input, options);
55
+ yield (0, parser_1.applySiteCodingAsync)(internal, options);
56
+ return buildParseResult(internal, options);
57
+ });
58
+ }
59
+ function formatSig(dosage, style = "short", options) {
60
+ const internal = (0, fhir_1.internalFromFhir)(dosage);
61
+ const localization = (0, i18n_1.resolveSigLocalization)(options === null || options === void 0 ? void 0 : options.locale, options === null || options === void 0 ? void 0 : options.i18n);
62
+ return (0, format_1.formatInternal)(internal, style, localization);
63
+ }
64
+ function fromFhirDosage(dosage, options) {
65
+ var _a, _b, _c;
66
+ const internal = (0, fhir_1.internalFromFhir)(dosage);
67
+ const localization = (0, i18n_1.resolveSigLocalization)(options === null || options === void 0 ? void 0 : options.locale, options === null || options === void 0 ? void 0 : options.i18n);
68
+ const shortText = (0, format_1.formatInternal)(internal, "short", localization);
69
+ const computedLong = (0, format_1.formatInternal)(internal, "long", localization);
70
+ const longText = localization ? computedLong : (_a = dosage.text) !== null && _a !== void 0 ? _a : computedLong;
71
+ return {
72
+ fhir: dosage,
73
+ shortText,
74
+ longText,
75
+ warnings: [],
76
+ meta: {
77
+ consumedTokens: [],
78
+ normalized: {
79
+ route: internal.routeCode,
80
+ unit: internal.unit,
81
+ site: internal.siteText || ((_b = internal.siteCoding) === null || _b === void 0 ? void 0 : _b.code)
82
+ ? {
83
+ text: internal.siteText,
84
+ coding: ((_c = internal.siteCoding) === null || _c === void 0 ? void 0 : _c.code)
85
+ ? {
86
+ code: internal.siteCoding.code,
87
+ display: internal.siteCoding.display,
88
+ system: internal.siteCoding.system
89
+ }
90
+ : undefined
91
+ }
92
+ : undefined
93
+ }
94
+ }
95
+ };
96
+ }
97
+ function buildParseResult(internal, options) {
98
+ var _a;
39
99
  const localization = (0, i18n_1.resolveSigLocalization)(options === null || options === void 0 ? void 0 : options.locale, options === null || options === void 0 ? void 0 : options.i18n);
40
100
  const shortText = (0, format_1.formatInternal)(internal, "short", localization);
41
101
  const longText = (0, format_1.formatInternal)(internal, "long", localization);
@@ -47,6 +107,26 @@ function parseSig(input, options) {
47
107
  .filter((token) => internal.consumed.has(token.index))
48
108
  .map((token) => token.original);
49
109
  const leftoverTokens = internal.tokens.filter((token) => !internal.consumed.has(token.index));
110
+ const siteCoding = ((_a = internal.siteCoding) === null || _a === void 0 ? void 0 : _a.code)
111
+ ? {
112
+ code: internal.siteCoding.code,
113
+ display: internal.siteCoding.display,
114
+ system: internal.siteCoding.system
115
+ }
116
+ : undefined;
117
+ const siteLookups = internal.siteLookups.length
118
+ ? internal.siteLookups.map((entry) => ({
119
+ request: entry.request,
120
+ suggestions: entry.suggestions.map((suggestion) => ({
121
+ coding: {
122
+ code: suggestion.coding.code,
123
+ display: suggestion.coding.display,
124
+ system: suggestion.coding.system
125
+ },
126
+ text: suggestion.text
127
+ }))
128
+ }))
129
+ : undefined;
50
130
  return {
51
131
  fhir,
52
132
  shortText,
@@ -59,34 +139,15 @@ function parseSig(input, options) {
59
139
  : undefined,
60
140
  normalized: {
61
141
  route: internal.routeCode,
62
- unit: internal.unit
63
- }
64
- }
65
- };
66
- }
67
- function formatSig(dosage, style = "short", options) {
68
- const internal = (0, fhir_1.internalFromFhir)(dosage);
69
- const localization = (0, i18n_1.resolveSigLocalization)(options === null || options === void 0 ? void 0 : options.locale, options === null || options === void 0 ? void 0 : options.i18n);
70
- return (0, format_1.formatInternal)(internal, style, localization);
71
- }
72
- function fromFhirDosage(dosage, options) {
73
- var _a;
74
- const internal = (0, fhir_1.internalFromFhir)(dosage);
75
- const localization = (0, i18n_1.resolveSigLocalization)(options === null || options === void 0 ? void 0 : options.locale, options === null || options === void 0 ? void 0 : options.i18n);
76
- const shortText = (0, format_1.formatInternal)(internal, "short", localization);
77
- const computedLong = (0, format_1.formatInternal)(internal, "long", localization);
78
- const longText = localization ? computedLong : (_a = dosage.text) !== null && _a !== void 0 ? _a : computedLong;
79
- return {
80
- fhir: dosage,
81
- shortText,
82
- longText,
83
- warnings: [],
84
- meta: {
85
- consumedTokens: [],
86
- normalized: {
87
- route: internal.routeCode,
88
- unit: internal.unit
89
- }
142
+ unit: internal.unit,
143
+ site: internal.siteText || siteCoding
144
+ ? {
145
+ text: internal.siteText,
146
+ coding: siteCoding
147
+ }
148
+ : undefined
149
+ },
150
+ siteLookups
90
151
  }
91
152
  };
92
153
  }
@@ -1,4 +1,8 @@
1
- import { EventTiming, FhirDayOfWeek, FhirPeriodUnit, RouteCode } from "./types";
1
+ import { EventTiming, FhirCoding, FhirDayOfWeek, FhirPeriodUnit, RouteCode, SiteCodeLookupRequest, SiteCodeSuggestion } from "./types";
2
+ export interface SiteLookupDetail {
3
+ request: SiteCodeLookupRequest;
4
+ suggestions: SiteCodeSuggestion[];
5
+ }
2
6
  export interface Token {
3
7
  original: string;
4
8
  lower: string;
@@ -31,4 +35,7 @@ export interface ParsedSigInternal {
31
35
  siteText?: string;
32
36
  siteSource?: "abbreviation" | "text";
33
37
  siteTokenIndices: Set<number>;
38
+ siteCoding?: FhirCoding;
39
+ siteLookupRequest?: SiteCodeLookupRequest;
40
+ siteLookups: SiteLookupDetail[];
34
41
  }
package/dist/maps.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { EventTiming, FhirDayOfWeek, FhirPeriodUnit, RouteCode, SNOMEDCTRouteCodes } from "./types";
1
+ import { BodySiteDefinition, EventTiming, FhirDayOfWeek, FhirPeriodUnit, RouteCode, SNOMEDCTRouteCodes } from "./types";
2
2
  /**
3
3
  * SNOMED CT codings aligned with every known RouteCode. Keeping the structure
4
4
  * data-driven ensures any additions to the enumeration are surfaced
@@ -19,6 +19,13 @@ export interface RouteSynonym {
19
19
  text: string;
20
20
  }
21
21
  export declare const DEFAULT_ROUTE_SYNONYMS: Record<string, RouteSynonym>;
22
+ /**
23
+ * Normalizes body-site phrases into lookup keys by trimming, lower-casing, and
24
+ * collapsing whitespace. Custom site maps should normalize their keys with the
25
+ * same logic to ensure consistent lookups.
26
+ */
27
+ export declare function normalizeBodySiteKey(value: string): string;
28
+ export declare const DEFAULT_BODY_SITE_SNOMED: Record<string, BodySiteDefinition>;
22
29
  export declare const HOUSEHOLD_VOLUME_UNITS: readonly ["tsp", "tbsp"];
23
30
  export declare const DEFAULT_UNIT_SYNONYMS: Record<string, string>;
24
31
  export interface FrequencyDescriptor {