ezmedicationinput 0.1.9 → 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/dist/parser.js CHANGED
@@ -1,13 +1,25 @@
1
1
  "use strict";
2
+ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
3
+ function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
4
+ return new (P || (P = Promise))(function (resolve, reject) {
5
+ function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
6
+ function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
7
+ function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
8
+ step((generator = generator.apply(thisArg, _arguments || [])).next());
9
+ });
10
+ };
2
11
  Object.defineProperty(exports, "__esModule", { value: true });
3
12
  exports.tokenize = tokenize;
4
13
  exports.parseInternal = parseInternal;
14
+ exports.applySiteCoding = applySiteCoding;
15
+ exports.applySiteCodingAsync = applySiteCodingAsync;
5
16
  const maps_1 = require("./maps");
6
17
  const context_1 = require("./context");
7
18
  const safety_1 = require("./safety");
8
19
  const types_1 = require("./types");
9
20
  const object_1 = require("./utils/object");
10
21
  const array_1 = require("./utils/array");
22
+ const SNOMED_SYSTEM = "http://snomed.info/sct";
11
23
  const BODY_SITE_HINTS = new Set([
12
24
  "left",
13
25
  "right",
@@ -66,10 +78,14 @@ const BODY_SITE_HINTS = new Set([
66
78
  "veins",
67
79
  "vagina",
68
80
  "vaginal",
81
+ "penis",
82
+ "penile",
69
83
  "rectum",
70
84
  "rectal",
71
85
  "anus",
72
- "perineum"
86
+ "perineum",
87
+ "temple",
88
+ "temples"
73
89
  ]);
74
90
  const SITE_CONNECTORS = new Set(["to", "in", "into", "on", "onto", "at"]);
75
91
  const SITE_FILLER_WORDS = new Set([
@@ -241,7 +257,7 @@ const OPHTHALMIC_CONTEXT_TOKENS = new Set([
241
257
  "be"
242
258
  ]);
243
259
  function normalizeTokenLower(token) {
244
- return token.lower.replace(/\./g, "");
260
+ return token.lower.replace(/[.{}]/g, "");
245
261
  }
246
262
  function hasOphthalmicContextHint(tokens, index) {
247
263
  for (let offset = -3; offset <= 3; offset++) {
@@ -605,6 +621,68 @@ function tokenize(input) {
605
621
  }
606
622
  return tokens;
607
623
  }
624
+ /**
625
+ * Locates the span of the detected site tokens within the caller's original
626
+ * input so downstream consumers can highlight or replace the exact substring.
627
+ */
628
+ function computeSiteTextRange(input, tokens, indices) {
629
+ if (!indices.length) {
630
+ return undefined;
631
+ }
632
+ const lowerInput = input.toLowerCase();
633
+ let searchStart = 0;
634
+ let rangeStart;
635
+ let rangeEnd;
636
+ for (const tokenIndex of indices) {
637
+ const token = tokens[tokenIndex];
638
+ if (!token) {
639
+ continue;
640
+ }
641
+ const segment = token.original.trim();
642
+ if (!segment) {
643
+ continue;
644
+ }
645
+ const lowerSegment = segment.toLowerCase();
646
+ const foundIndex = lowerInput.indexOf(lowerSegment, searchStart);
647
+ if (foundIndex === -1) {
648
+ return undefined;
649
+ }
650
+ const segmentEnd = foundIndex + lowerSegment.length;
651
+ if (rangeStart === undefined) {
652
+ rangeStart = foundIndex;
653
+ }
654
+ rangeEnd = segmentEnd;
655
+ searchStart = segmentEnd;
656
+ }
657
+ if (rangeStart === undefined || rangeEnd === undefined) {
658
+ return undefined;
659
+ }
660
+ return { start: rangeStart, end: rangeEnd };
661
+ }
662
+ /**
663
+ * Prefers highlighting the sanitized site text when it can be located directly
664
+ * in the original input; otherwise falls back to the broader token-derived
665
+ * range.
666
+ */
667
+ function refineSiteRange(input, sanitized, tokenRange) {
668
+ if (!input) {
669
+ return tokenRange;
670
+ }
671
+ const trimmed = sanitized.trim();
672
+ if (!trimmed) {
673
+ return tokenRange;
674
+ }
675
+ const lowerInput = input.toLowerCase();
676
+ const lowerSanitized = trimmed.toLowerCase();
677
+ let startIndex = tokenRange ? lowerInput.indexOf(lowerSanitized, tokenRange.start) : -1;
678
+ if (startIndex === -1) {
679
+ startIndex = lowerInput.indexOf(lowerSanitized);
680
+ }
681
+ if (startIndex === -1) {
682
+ return tokenRange;
683
+ }
684
+ return { start: startIndex, end: startIndex + lowerSanitized.length };
685
+ }
608
686
  function splitToken(token) {
609
687
  if (/^[0-9]+(?:\.[0-9]+)?$/.test(token)) {
610
688
  return [token];
@@ -1169,7 +1247,8 @@ function parseInternal(input, options) {
1169
1247
  dayOfWeek: [],
1170
1248
  when: [],
1171
1249
  warnings: [],
1172
- siteTokenIndices: new Set()
1250
+ siteTokenIndices: new Set(),
1251
+ siteLookups: []
1173
1252
  };
1174
1253
  const context = (_a = options === null || options === void 0 ? void 0 : options.context) !== null && _a !== void 0 ? _a : undefined;
1175
1254
  const customRouteMap = (options === null || options === void 0 ? void 0 : options.routeMap)
@@ -1630,7 +1709,8 @@ function parseInternal(input, options) {
1630
1709
  const leftoverTokens = tokens.filter((t) => !internal.consumed.has(t.index));
1631
1710
  const siteCandidateIndices = new Set();
1632
1711
  for (const token of leftoverTokens) {
1633
- if (BODY_SITE_HINTS.has(token.lower)) {
1712
+ const normalized = normalizeTokenLower(token);
1713
+ if (BODY_SITE_HINTS.has(normalized)) {
1634
1714
  siteCandidateIndices.add(token.index);
1635
1715
  }
1636
1716
  }
@@ -1646,7 +1726,7 @@ function parseInternal(input, options) {
1646
1726
  if (!token) {
1647
1727
  break;
1648
1728
  }
1649
- const lower = token.lower;
1729
+ const lower = normalizeTokenLower(token);
1650
1730
  if (SITE_CONNECTORS.has(lower) ||
1651
1731
  BODY_SITE_HINTS.has(lower) ||
1652
1732
  ROUTE_DESCRIPTOR_FILLER_WORDS.has(lower)) {
@@ -1662,7 +1742,7 @@ function parseInternal(input, options) {
1662
1742
  if (!token) {
1663
1743
  break;
1664
1744
  }
1665
- const lower = token.lower;
1745
+ const lower = normalizeTokenLower(token);
1666
1746
  if (SITE_CONNECTORS.has(lower) ||
1667
1747
  BODY_SITE_HINTS.has(lower) ||
1668
1748
  ROUTE_DESCRIPTOR_FILLER_WORDS.has(lower)) {
@@ -1680,8 +1760,10 @@ function parseInternal(input, options) {
1680
1760
  if (!token) {
1681
1761
  continue;
1682
1762
  }
1683
- const lower = token.lower;
1684
- if (!SITE_CONNECTORS.has(lower) && !SITE_FILLER_WORDS.has(lower)) {
1763
+ const lower = normalizeTokenLower(token);
1764
+ const trimmed = token.original.trim();
1765
+ const isBraceToken = trimmed.length > 0 && /^[{}]+$/.test(trimmed);
1766
+ if (!isBraceToken && !SITE_CONNECTORS.has(lower) && !SITE_FILLER_WORDS.has(lower)) {
1685
1767
  displayWords.push(token.original);
1686
1768
  }
1687
1769
  mark(internal.consumed, token);
@@ -1691,16 +1773,45 @@ function parseInternal(input, options) {
1691
1773
  .join(" ")
1692
1774
  .trim();
1693
1775
  if (normalizedSite) {
1694
- const normalizedLower = normalizedSite.toLowerCase();
1695
- const strippedDescriptor = normalizeRouteDescriptorPhrase(normalizedLower);
1696
- const siteWords = normalizedLower.split(/\s+/).filter((word) => word.length > 0);
1697
- const hasNonSiteWords = siteWords.some((word) => !BODY_SITE_HINTS.has(word));
1698
- const shouldAttemptRouteDescriptor = strippedDescriptor !== normalizedLower || hasNonSiteWords || strippedDescriptor === "mouth";
1699
- const appliedRouteDescriptor = shouldAttemptRouteDescriptor && maybeApplyRouteDescriptor(normalizedSite);
1700
- if (!appliedRouteDescriptor) {
1701
- internal.siteText = normalizedSite;
1702
- if (!internal.siteSource) {
1703
- internal.siteSource = "text";
1776
+ const tokenRange = computeSiteTextRange(internal.input, tokens, sortedIndices);
1777
+ let sanitized = normalizedSite;
1778
+ let isProbe = false;
1779
+ const probeMatch = sanitized.match(/^\{(.+)}$/);
1780
+ if (probeMatch) {
1781
+ // `{site}` placeholders flag interactive lookups so consumers can prompt
1782
+ // for a coded selection even when the parser cannot resolve the entry.
1783
+ isProbe = true;
1784
+ sanitized = probeMatch[1];
1785
+ }
1786
+ // Remove stray braces and normalize whitespace so lookups and downstream
1787
+ // displays operate on a clean phrase.
1788
+ sanitized = sanitized.replace(/[{}]/g, " ").replace(/\s+/g, " ").trim();
1789
+ const range = refineSiteRange(internal.input, sanitized, tokenRange);
1790
+ const sourceText = range ? internal.input.slice(range.start, range.end) : undefined;
1791
+ internal.siteLookupRequest = {
1792
+ originalText: normalizedSite,
1793
+ text: sanitized,
1794
+ normalized: sanitized.toLowerCase(),
1795
+ canonical: sanitized ? (0, maps_1.normalizeBodySiteKey)(sanitized) : "",
1796
+ isProbe,
1797
+ inputText: internal.input,
1798
+ sourceText,
1799
+ range
1800
+ };
1801
+ if (sanitized) {
1802
+ const normalizedLower = sanitized.toLowerCase();
1803
+ const strippedDescriptor = normalizeRouteDescriptorPhrase(normalizedLower);
1804
+ const siteWords = normalizedLower.split(/\s+/).filter((word) => word.length > 0);
1805
+ const hasNonSiteWords = siteWords.some((word) => !BODY_SITE_HINTS.has(word));
1806
+ const shouldAttemptRouteDescriptor = strippedDescriptor !== normalizedLower || hasNonSiteWords || strippedDescriptor === "mouth";
1807
+ const appliedRouteDescriptor = shouldAttemptRouteDescriptor && maybeApplyRouteDescriptor(sanitized);
1808
+ if (!appliedRouteDescriptor) {
1809
+ // Preserve the clean site text for FHIR output and resolver context
1810
+ // whenever we keep the original phrase.
1811
+ internal.siteText = sanitized;
1812
+ if (!internal.siteSource) {
1813
+ internal.siteSource = "text";
1814
+ }
1704
1815
  }
1705
1816
  }
1706
1817
  }
@@ -1734,6 +1845,249 @@ function parseInternal(input, options) {
1734
1845
  }
1735
1846
  return internal;
1736
1847
  }
1848
+ /**
1849
+ * Resolves parsed site text against SNOMED dictionaries and synchronous
1850
+ * callbacks, applying the best match to the in-progress parse result.
1851
+ */
1852
+ function applySiteCoding(internal, options) {
1853
+ runSiteCodingResolutionSync(internal, options);
1854
+ }
1855
+ /**
1856
+ * Asynchronous counterpart to {@link applySiteCoding} that awaits resolver and
1857
+ * suggestion callbacks so remote terminology services can be used.
1858
+ */
1859
+ function applySiteCodingAsync(internal, options) {
1860
+ return __awaiter(this, void 0, void 0, function* () {
1861
+ yield runSiteCodingResolutionAsync(internal, options);
1862
+ });
1863
+ }
1864
+ /**
1865
+ * Attempts to resolve site codings using built-in dictionaries followed by any
1866
+ * provided synchronous resolvers. Suggestions are collected when resolution
1867
+ * fails or a `{probe}` placeholder requested an interactive lookup.
1868
+ */
1869
+ function runSiteCodingResolutionSync(internal, options) {
1870
+ internal.siteLookups = [];
1871
+ const request = internal.siteLookupRequest;
1872
+ if (!request) {
1873
+ return;
1874
+ }
1875
+ const canonical = request.canonical;
1876
+ const customDefinition = lookupBodySiteDefinition(options === null || options === void 0 ? void 0 : options.siteCodeMap, canonical);
1877
+ let resolution = customDefinition;
1878
+ if (!resolution) {
1879
+ // Allow synchronous resolver callbacks to claim the site.
1880
+ for (const resolver of toArray(options === null || options === void 0 ? void 0 : options.siteCodeResolvers)) {
1881
+ const result = resolver(request);
1882
+ if (isPromise(result)) {
1883
+ throw new Error("Site code resolver returned a Promise; use parseSigAsync for asynchronous site resolution.");
1884
+ }
1885
+ if (result) {
1886
+ resolution = result;
1887
+ break;
1888
+ }
1889
+ }
1890
+ }
1891
+ const defaultDefinition = canonical ? maps_1.DEFAULT_BODY_SITE_SNOMED[canonical] : undefined;
1892
+ if (!resolution && defaultDefinition) {
1893
+ // Fall back to bundled SNOMED lookups when no overrides claim the site.
1894
+ resolution = defaultDefinition;
1895
+ }
1896
+ if (resolution) {
1897
+ applySiteDefinition(internal, resolution);
1898
+ }
1899
+ else {
1900
+ internal.siteCoding = undefined;
1901
+ }
1902
+ const needsSuggestions = request.isProbe || !resolution;
1903
+ if (!needsSuggestions) {
1904
+ return;
1905
+ }
1906
+ const suggestionMap = new Map();
1907
+ if (customDefinition) {
1908
+ addSuggestionToMap(suggestionMap, definitionToSuggestion(customDefinition));
1909
+ }
1910
+ if (defaultDefinition) {
1911
+ addSuggestionToMap(suggestionMap, definitionToSuggestion(defaultDefinition));
1912
+ }
1913
+ for (const resolver of toArray(options === null || options === void 0 ? void 0 : options.siteCodeSuggestionResolvers)) {
1914
+ // Aggregates resolver suggestions while guarding against accidental async
1915
+ // usage, mirroring the behavior of site resolvers.
1916
+ const result = resolver(request);
1917
+ if (isPromise(result)) {
1918
+ throw new Error("Site code suggestion resolver returned a Promise; use parseSigAsync for asynchronous site suggestions.");
1919
+ }
1920
+ collectSuggestionResult(suggestionMap, result);
1921
+ }
1922
+ const suggestions = Array.from(suggestionMap.values());
1923
+ if (suggestions.length || request.isProbe) {
1924
+ internal.siteLookups.push({ request, suggestions });
1925
+ }
1926
+ }
1927
+ /**
1928
+ * Async version of {@link runSiteCodingResolutionSync} that awaits resolver
1929
+ * results and suggestion providers, enabling remote terminology services.
1930
+ */
1931
+ function runSiteCodingResolutionAsync(internal, options) {
1932
+ return __awaiter(this, void 0, void 0, function* () {
1933
+ internal.siteLookups = [];
1934
+ const request = internal.siteLookupRequest;
1935
+ if (!request) {
1936
+ return;
1937
+ }
1938
+ const canonical = request.canonical;
1939
+ const customDefinition = lookupBodySiteDefinition(options === null || options === void 0 ? void 0 : options.siteCodeMap, canonical);
1940
+ let resolution = customDefinition;
1941
+ if (!resolution) {
1942
+ // Await asynchronous resolver callbacks (e.g., HTTP terminology services).
1943
+ for (const resolver of toArray(options === null || options === void 0 ? void 0 : options.siteCodeResolvers)) {
1944
+ const result = yield resolver(request);
1945
+ if (result) {
1946
+ resolution = result;
1947
+ break;
1948
+ }
1949
+ }
1950
+ }
1951
+ const defaultDefinition = canonical ? maps_1.DEFAULT_BODY_SITE_SNOMED[canonical] : undefined;
1952
+ if (!resolution && defaultDefinition) {
1953
+ resolution = defaultDefinition;
1954
+ }
1955
+ if (resolution) {
1956
+ applySiteDefinition(internal, resolution);
1957
+ }
1958
+ else {
1959
+ internal.siteCoding = undefined;
1960
+ }
1961
+ const needsSuggestions = request.isProbe || !resolution;
1962
+ if (!needsSuggestions) {
1963
+ return;
1964
+ }
1965
+ const suggestionMap = new Map();
1966
+ if (customDefinition) {
1967
+ addSuggestionToMap(suggestionMap, definitionToSuggestion(customDefinition));
1968
+ }
1969
+ if (defaultDefinition) {
1970
+ addSuggestionToMap(suggestionMap, definitionToSuggestion(defaultDefinition));
1971
+ }
1972
+ for (const resolver of toArray(options === null || options === void 0 ? void 0 : options.siteCodeSuggestionResolvers)) {
1973
+ // Async suggestion providers are awaited, allowing UI workflows to fetch
1974
+ // candidate codes on demand.
1975
+ const result = yield resolver(request);
1976
+ collectSuggestionResult(suggestionMap, result);
1977
+ }
1978
+ const suggestions = Array.from(suggestionMap.values());
1979
+ if (suggestions.length || request.isProbe) {
1980
+ internal.siteLookups.push({ request, suggestions });
1981
+ }
1982
+ });
1983
+ }
1984
+ /**
1985
+ * Looks up a body-site definition in a caller-provided map, honoring both
1986
+ * direct keys and entries that normalize to the same canonical phrase.
1987
+ */
1988
+ function lookupBodySiteDefinition(map, canonical) {
1989
+ if (!map) {
1990
+ return undefined;
1991
+ }
1992
+ const direct = map[canonical];
1993
+ if (direct) {
1994
+ return direct;
1995
+ }
1996
+ for (const [key, definition] of (0, object_1.objectEntries)(map)) {
1997
+ if ((0, maps_1.normalizeBodySiteKey)(key) === canonical) {
1998
+ return definition;
1999
+ }
2000
+ }
2001
+ return undefined;
2002
+ }
2003
+ /**
2004
+ * Applies the selected body-site definition onto the parser state, defaulting
2005
+ * the coding system to SNOMED CT when the definition omits one.
2006
+ */
2007
+ function applySiteDefinition(internal, definition) {
2008
+ var _a;
2009
+ const coding = definition.coding;
2010
+ internal.siteCoding = {
2011
+ code: coding.code,
2012
+ display: coding.display,
2013
+ system: (_a = coding.system) !== null && _a !== void 0 ? _a : SNOMED_SYSTEM
2014
+ };
2015
+ if (definition.text) {
2016
+ internal.siteText = definition.text;
2017
+ }
2018
+ }
2019
+ /**
2020
+ * Converts a body-site definition into a suggestion payload so all suggestion
2021
+ * sources share consistent structure.
2022
+ */
2023
+ function definitionToSuggestion(definition) {
2024
+ var _a;
2025
+ return {
2026
+ coding: {
2027
+ code: definition.coding.code,
2028
+ display: definition.coding.display,
2029
+ system: (_a = definition.coding.system) !== null && _a !== void 0 ? _a : SNOMED_SYSTEM
2030
+ },
2031
+ text: definition.text
2032
+ };
2033
+ }
2034
+ /**
2035
+ * Inserts a suggestion into a deduplicated map keyed by system and code.
2036
+ */
2037
+ function addSuggestionToMap(map, suggestion) {
2038
+ var _a, _b;
2039
+ if (!suggestion) {
2040
+ return;
2041
+ }
2042
+ const coding = suggestion.coding;
2043
+ if (!(coding === null || coding === void 0 ? void 0 : coding.code)) {
2044
+ return;
2045
+ }
2046
+ const key = `${(_a = coding.system) !== null && _a !== void 0 ? _a : SNOMED_SYSTEM}|${coding.code}`;
2047
+ if (!map.has(key)) {
2048
+ map.set(key, {
2049
+ coding: {
2050
+ code: coding.code,
2051
+ display: coding.display,
2052
+ system: (_b = coding.system) !== null && _b !== void 0 ? _b : SNOMED_SYSTEM
2053
+ },
2054
+ text: suggestion.text
2055
+ });
2056
+ }
2057
+ }
2058
+ /**
2059
+ * Normalizes resolver outputs into a consistent array before merging them into
2060
+ * the suggestion map.
2061
+ */
2062
+ function collectSuggestionResult(map, result) {
2063
+ if (!result) {
2064
+ return;
2065
+ }
2066
+ const suggestions = Array.isArray(result)
2067
+ ? result
2068
+ : typeof result === "object" && "suggestions" in result
2069
+ ? result.suggestions
2070
+ : [result];
2071
+ for (const suggestion of suggestions) {
2072
+ addSuggestionToMap(map, suggestion);
2073
+ }
2074
+ }
2075
+ /**
2076
+ * Wraps scalar or array configuration into an array to simplify iteration.
2077
+ */
2078
+ function toArray(value) {
2079
+ if (!value) {
2080
+ return [];
2081
+ }
2082
+ return Array.isArray(value) ? value : [value];
2083
+ }
2084
+ /**
2085
+ * Detects thenables without relying on `instanceof Promise`, which can break
2086
+ * across execution contexts.
2087
+ */
2088
+ function isPromise(value) {
2089
+ return !!value && typeof value.then === "function";
2090
+ }
1737
2091
  function normalizeUnit(token, options) {
1738
2092
  var _a;
1739
2093
  const override = enforceHouseholdUnitPolicy((_a = options === null || options === void 0 ? void 0 : options.unitMap) === null || _a === void 0 ? void 0 : _a[token], options);
package/dist/types.d.ts CHANGED
@@ -289,6 +289,64 @@ export interface FormatOptions {
289
289
  locale?: "en" | "th" | string;
290
290
  i18n?: SigTranslationConfig;
291
291
  }
292
+ export interface BodySiteCode {
293
+ code: string;
294
+ display?: string;
295
+ system?: string;
296
+ }
297
+ export interface BodySiteDefinition {
298
+ coding: BodySiteCode;
299
+ text?: string;
300
+ }
301
+ export interface TextRange {
302
+ /** Inclusive start index of the matched substring within the original input. */
303
+ start: number;
304
+ /** Exclusive end index of the matched substring within the original input. */
305
+ end: number;
306
+ }
307
+ export interface SiteCodeLookupRequest {
308
+ /** Original site text preserved for debugging or auditing. */
309
+ originalText: string;
310
+ /**
311
+ * Sanitized site text used for human-readable output. Connectors and braces
312
+ * are stripped but casing is preserved.
313
+ */
314
+ text: string;
315
+ /** Lower-case variant of the text for case-insensitive lookups. */
316
+ normalized: string;
317
+ /** Canonical key generated by trimming and collapsing whitespace. */
318
+ canonical: string;
319
+ /** Indicates the text was wrapped in `{}` to request interactive lookup. */
320
+ isProbe: boolean;
321
+ /** Full original input string provided to the parser. */
322
+ inputText: string;
323
+ /**
324
+ * Substring captured directly from the original input, preserving spacing and
325
+ * casing. Undefined when a reliable slice cannot be determined.
326
+ */
327
+ sourceText?: string;
328
+ /** Location of {@link sourceText} relative to the original input. */
329
+ range?: TextRange;
330
+ }
331
+ export interface SiteCodeResolution extends BodySiteDefinition {
332
+ }
333
+ export interface SiteCodeSuggestion {
334
+ coding: BodySiteCode;
335
+ text?: string;
336
+ }
337
+ export interface SiteCodeSuggestionsResult {
338
+ suggestions: SiteCodeSuggestion[];
339
+ }
340
+ /**
341
+ * Site code resolvers can perform deterministic lookups or remote queries with
342
+ * access to the original sig text and extracted site range.
343
+ */
344
+ export type SiteCodeResolver = (request: SiteCodeLookupRequest) => SiteCodeResolution | null | undefined | Promise<SiteCodeResolution | null | undefined>;
345
+ /**
346
+ * Suggestion providers receive the same context as resolvers, including the
347
+ * caller's full input and the character range of the detected site phrase.
348
+ */
349
+ export type SiteCodeSuggestionResolver = (request: SiteCodeLookupRequest) => SiteCodeSuggestionsResult | SiteCodeSuggestion[] | SiteCodeSuggestion | null | undefined | Promise<SiteCodeSuggestionsResult | SiteCodeSuggestion[] | SiteCodeSuggestion | null | undefined>;
292
350
  export interface ParseOptions extends FormatOptions {
293
351
  /**
294
352
  * Optional medication context that assists with default unit inference.
@@ -326,6 +384,23 @@ export interface ParseOptions extends FormatOptions {
326
384
  * and tablespoon when set to false. Defaults to true.
327
385
  */
328
386
  allowHouseholdVolumeUnits?: boolean;
387
+ /**
388
+ * Allows mapping normalized site phrases (e.g., "left arm") to
389
+ * institution-specific codings. Keys are normalized with the same logic as
390
+ * the default site dictionary (trimmed, lower-cased, collapsing whitespace).
391
+ */
392
+ siteCodeMap?: Record<string, BodySiteDefinition>;
393
+ /**
394
+ * Callback(s) that can translate detected site text into a coded body site.
395
+ * Return a promise when using asynchronous terminology services.
396
+ */
397
+ siteCodeResolvers?: SiteCodeResolver | SiteCodeResolver[];
398
+ /**
399
+ * Callback(s) that surface possible coded body sites for interactive flows
400
+ * when the parser cannot confidently resolve a site, or the input explicitly
401
+ * requested a lookup via `{site}` placeholders.
402
+ */
403
+ siteCodeSuggestionResolvers?: SiteCodeSuggestionResolver | SiteCodeSuggestionResolver[];
329
404
  }
330
405
  export interface ParseResult {
331
406
  fhir: FhirDosage;
@@ -338,7 +413,15 @@ export interface ParseResult {
338
413
  normalized: {
339
414
  route?: RouteCode;
340
415
  unit?: string;
416
+ site?: {
417
+ text?: string;
418
+ coding?: BodySiteCode;
419
+ };
341
420
  };
421
+ siteLookups?: Array<{
422
+ request: SiteCodeLookupRequest;
423
+ suggestions: SiteCodeSuggestion[];
424
+ }>;
342
425
  };
343
426
  }
344
427
  /**
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ezmedicationinput",
3
- "version": "0.1.9",
3
+ "version": "0.1.11",
4
4
  "description": "Parse concise medication sigs into FHIR R5 Dosage JSON",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",