ezmedicationinput 0.1.13 → 0.1.14

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
@@ -138,9 +138,25 @@ const result = await parseSigAsync("apply to {left temple} nightly", {
138
138
  system: "http://example.org/custom",
139
139
  code: "LTEMP",
140
140
  display: "Left temple"
141
- }
141
+ },
142
+ aliases: ["temporal region, left"],
143
+ text: "Left temple"
142
144
  }
143
145
  },
146
+ // any overrides that the user explicitly selected
147
+ siteCodeSelections: [
148
+ {
149
+ canonical: "scalp",
150
+ resolution: {
151
+ coding: {
152
+ system: "http://snomed.info/sct",
153
+ code: "39937001",
154
+ display: "Scalp structure"
155
+ },
156
+ text: "Scalp"
157
+ }
158
+ }
159
+ ],
144
160
  siteCodeResolvers: async (request) => {
145
161
  if (request.canonical === "mole on scalp") {
146
162
  return {
@@ -171,11 +187,13 @@ result.meta.siteLookups;
171
187
  // → [{ request: { text: "left temple", isProbe: true, ... }, suggestions: [...] }]
172
188
  ```
173
189
 
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.
190
+ - `siteCodeMap` lets you supply deterministic overrides for normalized site phrases.
191
+ - Entries accept an `aliases` array so punctuation-heavy variants (e.g., "first bicuspid, left") can resolve to the same coding.
192
+ - `siteCodeResolvers` (sync or async) can call external services to resolve sites on demand.
193
+ - `siteCodeSuggestionResolvers` return candidate codes; their results populate `meta.siteLookups[0].suggestions`.
194
+ - `siteCodeSelections` let callers override the automatic match for a detected phrase or range—helpful when a clinician chooses a bundled SNOMED option over a custom override.
195
+ - 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.
196
+ - `parseSigAsync` behaves like `parseSig` but awaits asynchronous resolvers and suggestion providers.
179
197
 
180
198
  #### Site resolver signatures
181
199
 
@@ -225,6 +243,7 @@ You can specify the number of times (total count) the medication is supposed to
225
243
  - `allowHouseholdVolumeUnits`: defaults to `true`; set to `false` to ignore
226
244
  teaspoon/tablespoon units during parsing and suggestions.
227
245
  - Custom `routeMap`, `unitMap`, `freqMap`, and `whenMap` let you augment the built-in dictionaries without mutating them.
246
+ - `siteCodeSelections` override automatic site resolution for matching phrases or ranges so user-picked suggestions stick when re-parsing a sig.
228
247
 
229
248
  ### Next due dose generation
230
249
 
package/dist/maps.js CHANGED
@@ -170,7 +170,12 @@ exports.DEFAULT_ROUTE_SYNONYMS = (() => {
170
170
  * same logic to ensure consistent lookups.
171
171
  */
172
172
  function normalizeBodySiteKey(value) {
173
- return value.trim().toLowerCase().replace(/\s+/g, " ");
173
+ return value
174
+ .trim()
175
+ .toLowerCase()
176
+ .replace(/[^\p{L}\p{N}]+/gu, " ")
177
+ .replace(/\s+/g, " ")
178
+ .trim();
174
179
  }
175
180
  const DEFAULT_BODY_SITE_SNOMED_SOURCE = [
176
181
  {
package/dist/parser.js CHANGED
@@ -25,16 +25,27 @@ function buildCustomSiteHints(map) {
25
25
  return undefined;
26
26
  }
27
27
  const hints = new Set();
28
- for (const key of Object.keys(map)) {
29
- const normalized = (0, maps_1.normalizeBodySiteKey)(key);
28
+ const addPhraseHints = (phrase) => {
29
+ if (!phrase) {
30
+ return;
31
+ }
32
+ const normalized = (0, maps_1.normalizeBodySiteKey)(phrase);
30
33
  if (!normalized) {
31
- continue;
34
+ return;
32
35
  }
33
36
  for (const part of normalized.split(" ")) {
34
37
  if (part) {
35
38
  hints.add(part);
36
39
  }
37
40
  }
41
+ };
42
+ for (const [key, definition] of (0, object_1.objectEntries)(map)) {
43
+ addPhraseHints(key);
44
+ if (definition.aliases) {
45
+ for (const alias of definition.aliases) {
46
+ addPhraseHints(alias);
47
+ }
48
+ }
38
49
  }
39
50
  return hints;
40
51
  }
@@ -1896,8 +1907,9 @@ function runSiteCodingResolutionSync(internal, options) {
1896
1907
  return;
1897
1908
  }
1898
1909
  const canonical = request.canonical;
1910
+ const selection = pickSiteSelection(options === null || options === void 0 ? void 0 : options.siteCodeSelections, request);
1899
1911
  const customDefinition = lookupBodySiteDefinition(options === null || options === void 0 ? void 0 : options.siteCodeMap, canonical);
1900
- let resolution = customDefinition;
1912
+ let resolution = selection !== null && selection !== void 0 ? selection : customDefinition;
1901
1913
  if (!resolution) {
1902
1914
  // Allow synchronous resolver callbacks to claim the site.
1903
1915
  for (const resolver of toArray(options === null || options === void 0 ? void 0 : options.siteCodeResolvers)) {
@@ -1927,6 +1939,9 @@ function runSiteCodingResolutionSync(internal, options) {
1927
1939
  return;
1928
1940
  }
1929
1941
  const suggestionMap = new Map();
1942
+ if (selection) {
1943
+ addSuggestionToMap(suggestionMap, definitionToSuggestion(selection));
1944
+ }
1930
1945
  if (customDefinition) {
1931
1946
  addSuggestionToMap(suggestionMap, definitionToSuggestion(customDefinition));
1932
1947
  }
@@ -1959,8 +1974,9 @@ function runSiteCodingResolutionAsync(internal, options) {
1959
1974
  return;
1960
1975
  }
1961
1976
  const canonical = request.canonical;
1977
+ const selection = pickSiteSelection(options === null || options === void 0 ? void 0 : options.siteCodeSelections, request);
1962
1978
  const customDefinition = lookupBodySiteDefinition(options === null || options === void 0 ? void 0 : options.siteCodeMap, canonical);
1963
- let resolution = customDefinition;
1979
+ let resolution = selection !== null && selection !== void 0 ? selection : customDefinition;
1964
1980
  if (!resolution) {
1965
1981
  // Await asynchronous resolver callbacks (e.g., HTTP terminology services).
1966
1982
  for (const resolver of toArray(options === null || options === void 0 ? void 0 : options.siteCodeResolvers)) {
@@ -1986,6 +2002,9 @@ function runSiteCodingResolutionAsync(internal, options) {
1986
2002
  return;
1987
2003
  }
1988
2004
  const suggestionMap = new Map();
2005
+ if (selection) {
2006
+ addSuggestionToMap(suggestionMap, definitionToSuggestion(selection));
2007
+ }
1989
2008
  if (customDefinition) {
1990
2009
  addSuggestionToMap(suggestionMap, definitionToSuggestion(customDefinition));
1991
2010
  }
@@ -2020,6 +2039,57 @@ function lookupBodySiteDefinition(map, canonical) {
2020
2039
  if ((0, maps_1.normalizeBodySiteKey)(key) === canonical) {
2021
2040
  return definition;
2022
2041
  }
2042
+ if (definition.aliases) {
2043
+ for (const alias of definition.aliases) {
2044
+ if ((0, maps_1.normalizeBodySiteKey)(alias) === canonical) {
2045
+ return definition;
2046
+ }
2047
+ }
2048
+ }
2049
+ }
2050
+ return undefined;
2051
+ }
2052
+ function pickSiteSelection(selections, request) {
2053
+ if (!selections) {
2054
+ return undefined;
2055
+ }
2056
+ const canonical = request.canonical;
2057
+ const normalizedText = (0, maps_1.normalizeBodySiteKey)(request.text);
2058
+ const requestRange = request.range;
2059
+ for (const selection of toArray(selections)) {
2060
+ if (!selection) {
2061
+ continue;
2062
+ }
2063
+ let matched = false;
2064
+ if (selection.range) {
2065
+ if (!requestRange) {
2066
+ continue;
2067
+ }
2068
+ if (selection.range.start !== requestRange.start ||
2069
+ selection.range.end !== requestRange.end) {
2070
+ continue;
2071
+ }
2072
+ matched = true;
2073
+ }
2074
+ if (selection.canonical) {
2075
+ if ((0, maps_1.normalizeBodySiteKey)(selection.canonical) !== canonical) {
2076
+ continue;
2077
+ }
2078
+ matched = true;
2079
+ }
2080
+ else if (selection.text) {
2081
+ const normalizedSelection = (0, maps_1.normalizeBodySiteKey)(selection.text);
2082
+ if (normalizedSelection !== canonical && normalizedSelection !== normalizedText) {
2083
+ continue;
2084
+ }
2085
+ matched = true;
2086
+ }
2087
+ if (!selection.range && !selection.canonical && !selection.text) {
2088
+ continue;
2089
+ }
2090
+ if (matched) {
2091
+ return selection.resolution;
2092
+ }
2023
2093
  }
2024
2094
  return undefined;
2025
2095
  }
package/dist/types.d.ts CHANGED
@@ -297,6 +297,12 @@ export interface BodySiteCode {
297
297
  export interface BodySiteDefinition {
298
298
  coding: BodySiteCode;
299
299
  text?: string;
300
+ /**
301
+ * Optional phrases that should resolve to the same coding as this entry.
302
+ * Aliases are normalized with the same logic as map keys so callers can
303
+ * provide punctuation-heavy variants such as "first bicuspid, left".
304
+ */
305
+ aliases?: string[];
300
306
  }
301
307
  export interface TextRange {
302
308
  /** Inclusive start index of the matched substring within the original input. */
@@ -337,6 +343,24 @@ export interface SiteCodeSuggestion {
337
343
  export interface SiteCodeSuggestionsResult {
338
344
  suggestions: SiteCodeSuggestion[];
339
345
  }
346
+ /**
347
+ * Allows callers to override the parser's automatic site resolution for a
348
+ * specific match. Matches can be scoped by the normalized phrase, the original
349
+ * sanitized text, or the exact character range that was detected.
350
+ */
351
+ export interface SiteCodeSelection {
352
+ /** Canonical key (punctuation stripped, lower-case) that should trigger this selection. */
353
+ canonical?: string;
354
+ /**
355
+ * Human-friendly site text used to match the extracted phrase. It is
356
+ * normalized with the same logic as canonical keys.
357
+ */
358
+ text?: string;
359
+ /** Exact range of the detected phrase within the input string. */
360
+ range?: TextRange;
361
+ /** Desired coded definition to apply when the selection matches. */
362
+ resolution: SiteCodeResolution;
363
+ }
340
364
  /**
341
365
  * Site code resolvers can perform deterministic lookups or remote queries with
342
366
  * access to the original sig text and extracted site range.
@@ -390,6 +414,12 @@ export interface ParseOptions extends FormatOptions {
390
414
  * the default site dictionary (trimmed, lower-cased, collapsing whitespace).
391
415
  */
392
416
  siteCodeMap?: Record<string, BodySiteDefinition>;
417
+ /**
418
+ * Explicit selections that override automatic site resolution for matching
419
+ * phrases. Useful when custom dictionaries provide multiple options but a UI
420
+ * workflow needs to pin a particular coding for a given match or range.
421
+ */
422
+ siteCodeSelections?: SiteCodeSelection | SiteCodeSelection[];
393
423
  /**
394
424
  * Callback(s) that can translate detected site text into a coded body site.
395
425
  * Return a promise when using asynchronous terminology services.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ezmedicationinput",
3
- "version": "0.1.13",
3
+ "version": "0.1.14",
4
4
  "description": "Parse concise medication sigs into FHIR R5 Dosage JSON",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",