ezmedicationinput 0.1.16 → 0.1.18
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 +56 -0
- package/dist/format.js +13 -0
- package/dist/maps.js +2 -1
- package/dist/parser.js +473 -47
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
- Emits timing abbreviations (`timing.code`) and repeat structures simultaneously where possible.
|
|
9
9
|
- Maps meal/time blocks to the correct `Timing.repeat.when` **EventTiming** codes and can auto-expand AC/PC/C into specific meals.
|
|
10
10
|
- Outputs SNOMED CT route codings (while providing friendly text) and round-trips known SNOMED routes back into the parser.
|
|
11
|
+
- Auto-codes common PRN (as-needed) reasons and additional dosage instructions while keeping the raw text when no coding is available.
|
|
11
12
|
- Understands ocular and intravitreal shorthand (OD/OS/OU, LE/RE/BE, IVT*, VOD/VOS, etc.) and warns when intravitreal instructions omit an eye side.
|
|
12
13
|
- Parses fractional/ minute-based intervals (`q0.5h`, `q30 min`, `q1/4hr`) plus dose and timing ranges.
|
|
13
14
|
- Supports extensible dictionaries for routes, units, frequency shorthands, and event timing tokens.
|
|
@@ -50,6 +51,59 @@ Example output:
|
|
|
50
51
|
}
|
|
51
52
|
```
|
|
52
53
|
|
|
54
|
+
### PRN reasons & additional instructions
|
|
55
|
+
|
|
56
|
+
`parseSig` identifies PRN (as-needed) clauses and trailing instructions, then
|
|
57
|
+
codes them with SNOMED CT whenever possible.
|
|
58
|
+
|
|
59
|
+
```ts
|
|
60
|
+
const result = parseSig("1 tab po q4h prn headache; do not exceed 6 tabs/day");
|
|
61
|
+
|
|
62
|
+
result.fhir.asNeededFor;
|
|
63
|
+
// → [{
|
|
64
|
+
// text: "headache",
|
|
65
|
+
// coding: [{
|
|
66
|
+
// system: "http://snomed.info/sct",
|
|
67
|
+
// code: "25064002",
|
|
68
|
+
// display: "Headache"
|
|
69
|
+
// }]
|
|
70
|
+
// }]
|
|
71
|
+
|
|
72
|
+
result.fhir.additionalInstruction;
|
|
73
|
+
// → [{ text: "Do not exceed 6 tablets daily" }]
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
Customize the dictionaries and lookups through `ParseOptions`:
|
|
77
|
+
|
|
78
|
+
```ts
|
|
79
|
+
parseSig(input, {
|
|
80
|
+
prnReasonMap: {
|
|
81
|
+
migraine: {
|
|
82
|
+
text: "Migraine",
|
|
83
|
+
coding: {
|
|
84
|
+
system: "http://snomed.info/sct",
|
|
85
|
+
code: "37796009",
|
|
86
|
+
display: "Migraine"
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
},
|
|
90
|
+
prnReasonResolvers: async (request) => terminologyService.lookup(request),
|
|
91
|
+
prnReasonSuggestionResolvers: async (request) => terminologyService.suggest(request),
|
|
92
|
+
});
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
Use `{reason}` in the sig string (e.g. `prn {migraine}`) to force a lookup even
|
|
96
|
+
when a direct match exists. Additional instructions are sourced from a built-in
|
|
97
|
+
set of SNOMED CT concepts under *419492006 – Additional dosage instructions* and
|
|
98
|
+
fall back to plain text when no coding is available. Parsed instructions are
|
|
99
|
+
also echoed in `ParseResult.meta.normalized.additionalInstructions` for quick UI
|
|
100
|
+
rendering.
|
|
101
|
+
|
|
102
|
+
When a PRN reason cannot be auto-resolved, any registered suggestion resolvers
|
|
103
|
+
are invoked and their responses are surfaced through
|
|
104
|
+
`ParseResult.meta.prnReasonLookups` so client applications can prompt the user
|
|
105
|
+
to choose a coded concept.
|
|
106
|
+
|
|
53
107
|
### Sig (directions) suggestions
|
|
54
108
|
|
|
55
109
|
Use `suggestSig` to drive autocomplete experiences while the clinician is
|
|
@@ -126,6 +180,8 @@ result.fhir.site?.coding?.[0];
|
|
|
126
180
|
|
|
127
181
|
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
182
|
|
|
183
|
+
Unknown body sites still populate `Dosage.site.text` and `ParseResult.meta.normalized.site.text`, allowing UIs to echo the verbatim phrase while terminology lookups run asynchronously.
|
|
184
|
+
|
|
129
185
|
You can extend or replace the built-in codings via `ParseOptions`:
|
|
130
186
|
|
|
131
187
|
```ts
|
package/dist/format.js
CHANGED
|
@@ -69,6 +69,11 @@ const ROUTE_GRAMMAR = {
|
|
|
69
69
|
routePhrase: ({ hasSite }) => (hasSite ? undefined : "into the eye"),
|
|
70
70
|
sitePreposition: "into"
|
|
71
71
|
},
|
|
72
|
+
[types_1.RouteCode["Per rectum"]]: {
|
|
73
|
+
verb: "Use",
|
|
74
|
+
routePhrase: ({ hasSite }) => (hasSite ? undefined : "rectally"),
|
|
75
|
+
sitePreposition: "into"
|
|
76
|
+
},
|
|
72
77
|
[types_1.RouteCode["Topical route"]]: {
|
|
73
78
|
verb: "Apply",
|
|
74
79
|
routePhrase: ({ hasSite }) => (hasSite ? undefined : "topically"),
|
|
@@ -137,6 +142,9 @@ function grammarFromRouteText(text) {
|
|
|
137
142
|
if (normalized.includes("intravenous") || normalized === "iv") {
|
|
138
143
|
return ROUTE_GRAMMAR[types_1.RouteCode["Intravenous route"]];
|
|
139
144
|
}
|
|
145
|
+
if (normalized.includes("rectal") || normalized.includes("rectum")) {
|
|
146
|
+
return ROUTE_GRAMMAR[types_1.RouteCode["Per rectum"]];
|
|
147
|
+
}
|
|
140
148
|
if (normalized.includes("nasal")) {
|
|
141
149
|
return ROUTE_GRAMMAR[types_1.RouteCode["Nasal route"]];
|
|
142
150
|
}
|
|
@@ -412,6 +420,11 @@ function formatSite(internal, grammar) {
|
|
|
412
420
|
return undefined;
|
|
413
421
|
}
|
|
414
422
|
const lower = text.toLowerCase();
|
|
423
|
+
if (internal.routeCode === types_1.RouteCode["Per rectum"]) {
|
|
424
|
+
if (lower === "rectum" || lower === "rectal") {
|
|
425
|
+
return undefined;
|
|
426
|
+
}
|
|
427
|
+
}
|
|
415
428
|
let preposition = grammar.sitePreposition;
|
|
416
429
|
if (!preposition) {
|
|
417
430
|
if (lower.includes("eye")) {
|
package/dist/maps.js
CHANGED
|
@@ -1254,6 +1254,7 @@ exports.DEFAULT_UNIT_BY_ROUTE = (() => {
|
|
|
1254
1254
|
ensure(types_1.RouteCode["Otic route"], "drop");
|
|
1255
1255
|
ensure(types_1.RouteCode["Respiratory tract route (qualifier value)"], "puff");
|
|
1256
1256
|
ensure(types_1.RouteCode["Transdermal route"], "patch");
|
|
1257
|
+
ensure(types_1.RouteCode["Per rectum"], "suppository");
|
|
1257
1258
|
return resolved;
|
|
1258
1259
|
})();
|
|
1259
1260
|
function normalizePrnReasonKey(value) {
|
|
@@ -1281,7 +1282,7 @@ const DEFAULT_PRN_REASON_SOURCE = [
|
|
|
1281
1282
|
}
|
|
1282
1283
|
},
|
|
1283
1284
|
{
|
|
1284
|
-
names: ["nausea", "queasiness"],
|
|
1285
|
+
names: ["nausea", "queasiness", "vomiting", "n/v", "nausea and vomiting"],
|
|
1285
1286
|
definition: {
|
|
1286
1287
|
coding: { system: SNOMED_SYSTEM, code: "422587007", display: "Nausea" },
|
|
1287
1288
|
text: "Nausea"
|
package/dist/parser.js
CHANGED
|
@@ -1897,43 +1897,105 @@ function parseInternal(input, options) {
|
|
|
1897
1897
|
if (internal.asNeeded && prnReasonStart !== undefined) {
|
|
1898
1898
|
const reasonTokens = [];
|
|
1899
1899
|
const reasonIndices = [];
|
|
1900
|
+
const reasonObjects = [];
|
|
1900
1901
|
for (let i = prnReasonStart; i < tokens.length; i++) {
|
|
1901
1902
|
const token = tokens[i];
|
|
1902
1903
|
if (internal.consumed.has(token.index)) {
|
|
1903
|
-
|
|
1904
|
+
internal.consumed.delete(token.index);
|
|
1904
1905
|
}
|
|
1905
1906
|
reasonTokens.push(token.original);
|
|
1906
1907
|
reasonIndices.push(token.index);
|
|
1908
|
+
reasonObjects.push(token);
|
|
1907
1909
|
mark(internal.consumed, token);
|
|
1908
1910
|
}
|
|
1909
1911
|
if (reasonTokens.length > 0) {
|
|
1910
|
-
|
|
1911
|
-
|
|
1912
|
-
|
|
1913
|
-
|
|
1914
|
-
const
|
|
1915
|
-
|
|
1916
|
-
|
|
1917
|
-
|
|
1918
|
-
|
|
1919
|
-
|
|
1920
|
-
|
|
1912
|
+
let sortedIndices = reasonIndices.slice().sort((a, b) => a - b);
|
|
1913
|
+
let range = computeTokenRange(internal.input, tokens, sortedIndices);
|
|
1914
|
+
let sourceText = range ? internal.input.slice(range.start, range.end) : undefined;
|
|
1915
|
+
if (sourceText) {
|
|
1916
|
+
const cutoff = determinePrnReasonCutoff(reasonObjects, sourceText);
|
|
1917
|
+
if (cutoff !== undefined) {
|
|
1918
|
+
for (let i = cutoff; i < reasonObjects.length; i++) {
|
|
1919
|
+
internal.consumed.delete(reasonObjects[i].index);
|
|
1920
|
+
}
|
|
1921
|
+
reasonObjects.splice(cutoff);
|
|
1922
|
+
reasonTokens.splice(cutoff);
|
|
1923
|
+
reasonIndices.splice(cutoff);
|
|
1924
|
+
while (reasonTokens.length > 0) {
|
|
1925
|
+
const lastToken = reasonTokens[reasonTokens.length - 1];
|
|
1926
|
+
if (!lastToken || /^[;:.,-]+$/.test(lastToken.trim())) {
|
|
1927
|
+
const removedObject = reasonObjects.pop();
|
|
1928
|
+
if (removedObject) {
|
|
1929
|
+
internal.consumed.delete(removedObject.index);
|
|
1930
|
+
}
|
|
1931
|
+
reasonTokens.pop();
|
|
1932
|
+
const removedIndex = reasonIndices.pop();
|
|
1933
|
+
if (removedIndex !== undefined) {
|
|
1934
|
+
internal.consumed.delete(removedIndex);
|
|
1935
|
+
}
|
|
1936
|
+
continue;
|
|
1937
|
+
}
|
|
1938
|
+
break;
|
|
1939
|
+
}
|
|
1940
|
+
if (reasonTokens.length > 0) {
|
|
1941
|
+
sortedIndices = reasonIndices.slice().sort((a, b) => a - b);
|
|
1942
|
+
range = computeTokenRange(internal.input, tokens, sortedIndices);
|
|
1943
|
+
sourceText = range ? internal.input.slice(range.start, range.end) : undefined;
|
|
1944
|
+
}
|
|
1945
|
+
else {
|
|
1946
|
+
range = undefined;
|
|
1947
|
+
sourceText = undefined;
|
|
1948
|
+
}
|
|
1949
|
+
}
|
|
1950
|
+
}
|
|
1951
|
+
if (reasonTokens.length > 0) {
|
|
1952
|
+
const siteStart = findTrailingPrnSiteSuffix(reasonObjects, internal, options);
|
|
1953
|
+
if (siteStart !== undefined) {
|
|
1954
|
+
for (let i = siteStart; i < reasonObjects.length; i++) {
|
|
1955
|
+
internal.consumed.delete(reasonObjects[i].index);
|
|
1956
|
+
}
|
|
1957
|
+
reasonObjects.splice(siteStart);
|
|
1958
|
+
reasonTokens.splice(siteStart);
|
|
1959
|
+
reasonIndices.splice(siteStart);
|
|
1960
|
+
if (reasonTokens.length > 0) {
|
|
1961
|
+
sortedIndices = reasonIndices.slice().sort((a, b) => a - b);
|
|
1962
|
+
range = computeTokenRange(internal.input, tokens, sortedIndices);
|
|
1963
|
+
sourceText = range ? internal.input.slice(range.start, range.end) : undefined;
|
|
1964
|
+
}
|
|
1965
|
+
else {
|
|
1966
|
+
range = undefined;
|
|
1967
|
+
sourceText = undefined;
|
|
1968
|
+
}
|
|
1969
|
+
}
|
|
1970
|
+
}
|
|
1971
|
+
if (reasonTokens.length > 0) {
|
|
1972
|
+
const joined = reasonTokens.join(" ").trim();
|
|
1973
|
+
if (joined) {
|
|
1974
|
+
let sanitized = joined.replace(/\s+/g, " ").trim();
|
|
1975
|
+
let isProbe = false;
|
|
1976
|
+
const probeMatch = sanitized.match(/^\{(.+)}$/);
|
|
1977
|
+
if (probeMatch) {
|
|
1978
|
+
isProbe = true;
|
|
1979
|
+
sanitized = probeMatch[1];
|
|
1980
|
+
}
|
|
1981
|
+
sanitized = sanitized.replace(/[{}]/g, " ").replace(/\s+/g, " ").trim();
|
|
1982
|
+
const text = sanitized || joined;
|
|
1983
|
+
internal.asNeededReason = text;
|
|
1984
|
+
const normalized = text.toLowerCase();
|
|
1985
|
+
const canonical = sanitized
|
|
1986
|
+
? (0, maps_1.normalizePrnReasonKey)(sanitized)
|
|
1987
|
+
: (0, maps_1.normalizePrnReasonKey)(text);
|
|
1988
|
+
internal.prnReasonLookupRequest = {
|
|
1989
|
+
originalText: joined,
|
|
1990
|
+
text,
|
|
1991
|
+
normalized,
|
|
1992
|
+
canonical: canonical !== null && canonical !== void 0 ? canonical : "",
|
|
1993
|
+
isProbe,
|
|
1994
|
+
inputText: internal.input,
|
|
1995
|
+
sourceText,
|
|
1996
|
+
range
|
|
1997
|
+
};
|
|
1921
1998
|
}
|
|
1922
|
-
sanitized = sanitized.replace(/[{}]/g, " ").replace(/\s+/g, " ").trim();
|
|
1923
|
-
const text = sanitized || joined;
|
|
1924
|
-
internal.asNeededReason = text;
|
|
1925
|
-
const normalized = text.toLowerCase();
|
|
1926
|
-
const canonical = sanitized ? (0, maps_1.normalizePrnReasonKey)(sanitized) : (0, maps_1.normalizePrnReasonKey)(text);
|
|
1927
|
-
internal.prnReasonLookupRequest = {
|
|
1928
|
-
originalText: joined,
|
|
1929
|
-
text,
|
|
1930
|
-
normalized,
|
|
1931
|
-
canonical: canonical !== null && canonical !== void 0 ? canonical : "",
|
|
1932
|
-
isProbe,
|
|
1933
|
-
inputText: internal.input,
|
|
1934
|
-
sourceText,
|
|
1935
|
-
range
|
|
1936
|
-
};
|
|
1937
1999
|
}
|
|
1938
2000
|
}
|
|
1939
2001
|
}
|
|
@@ -2028,27 +2090,30 @@ function parseInternal(input, options) {
|
|
|
2028
2090
|
sanitized = sanitized.replace(/[{}]/g, " ").replace(/\s+/g, " ").trim();
|
|
2029
2091
|
const range = refineSiteRange(internal.input, sanitized, tokenRange);
|
|
2030
2092
|
const sourceText = range ? internal.input.slice(range.start, range.end) : undefined;
|
|
2093
|
+
const displayText = normalizeSiteDisplayText(sanitized, options === null || options === void 0 ? void 0 : options.siteCodeMap);
|
|
2094
|
+
const displayLower = displayText.toLowerCase();
|
|
2095
|
+
const canonical = displayText ? (0, maps_1.normalizeBodySiteKey)(displayText) : "";
|
|
2031
2096
|
internal.siteLookupRequest = {
|
|
2032
2097
|
originalText: normalizedSite,
|
|
2033
|
-
text:
|
|
2034
|
-
normalized:
|
|
2035
|
-
canonical
|
|
2098
|
+
text: displayText,
|
|
2099
|
+
normalized: displayLower,
|
|
2100
|
+
canonical,
|
|
2036
2101
|
isProbe,
|
|
2037
2102
|
inputText: internal.input,
|
|
2038
2103
|
sourceText,
|
|
2039
2104
|
range
|
|
2040
2105
|
};
|
|
2041
|
-
if (
|
|
2106
|
+
if (displayText) {
|
|
2042
2107
|
const normalizedLower = sanitized.toLowerCase();
|
|
2043
2108
|
const strippedDescriptor = normalizeRouteDescriptorPhrase(normalizedLower);
|
|
2044
|
-
const siteWords =
|
|
2109
|
+
const siteWords = displayLower.split(/\s+/).filter((word) => word.length > 0);
|
|
2045
2110
|
const hasNonSiteWords = siteWords.some((word) => !isBodySiteHint(word, internal.customSiteHints));
|
|
2046
2111
|
const shouldAttemptRouteDescriptor = strippedDescriptor !== normalizedLower || hasNonSiteWords || strippedDescriptor === "mouth";
|
|
2047
2112
|
const appliedRouteDescriptor = shouldAttemptRouteDescriptor && maybeApplyRouteDescriptor(sanitized);
|
|
2048
2113
|
if (!appliedRouteDescriptor) {
|
|
2049
2114
|
// Preserve the clean site text for FHIR output and resolver context
|
|
2050
2115
|
// whenever we keep the original phrase.
|
|
2051
|
-
internal.siteText =
|
|
2116
|
+
internal.siteText = displayText;
|
|
2052
2117
|
if (!internal.siteSource) {
|
|
2053
2118
|
internal.siteSource = "text";
|
|
2054
2119
|
}
|
|
@@ -2399,29 +2464,228 @@ function findAdditionalInstructionDefinition(text, canonical) {
|
|
|
2399
2464
|
}
|
|
2400
2465
|
return undefined;
|
|
2401
2466
|
}
|
|
2467
|
+
const BODY_SITE_ADJECTIVE_SUFFIXES = [
|
|
2468
|
+
"al",
|
|
2469
|
+
"ial",
|
|
2470
|
+
"ual",
|
|
2471
|
+
"ic",
|
|
2472
|
+
"ous",
|
|
2473
|
+
"ive",
|
|
2474
|
+
"ary",
|
|
2475
|
+
"ory",
|
|
2476
|
+
"atic",
|
|
2477
|
+
"etic",
|
|
2478
|
+
"ular",
|
|
2479
|
+
"otic",
|
|
2480
|
+
"ile",
|
|
2481
|
+
"eal",
|
|
2482
|
+
"inal",
|
|
2483
|
+
"aneal",
|
|
2484
|
+
"enal"
|
|
2485
|
+
];
|
|
2486
|
+
const DEFAULT_SITE_SYNONYM_KEYS = (() => {
|
|
2487
|
+
const map = new Map();
|
|
2488
|
+
for (const [key, definition] of (0, object_1.objectEntries)(maps_1.DEFAULT_BODY_SITE_SNOMED)) {
|
|
2489
|
+
if (!definition) {
|
|
2490
|
+
continue;
|
|
2491
|
+
}
|
|
2492
|
+
const normalized = key.trim();
|
|
2493
|
+
if (!normalized) {
|
|
2494
|
+
continue;
|
|
2495
|
+
}
|
|
2496
|
+
const existing = map.get(definition);
|
|
2497
|
+
if (existing) {
|
|
2498
|
+
if (existing.indexOf(normalized) === -1) {
|
|
2499
|
+
existing.push(normalized);
|
|
2500
|
+
}
|
|
2501
|
+
}
|
|
2502
|
+
else {
|
|
2503
|
+
map.set(definition, [normalized]);
|
|
2504
|
+
}
|
|
2505
|
+
}
|
|
2506
|
+
return map;
|
|
2507
|
+
})();
|
|
2508
|
+
function normalizeSiteDisplayText(text, customSiteMap) {
|
|
2509
|
+
var _a;
|
|
2510
|
+
const trimmed = text.trim();
|
|
2511
|
+
if (!trimmed) {
|
|
2512
|
+
return trimmed;
|
|
2513
|
+
}
|
|
2514
|
+
const canonicalInput = (0, maps_1.normalizeBodySiteKey)(trimmed);
|
|
2515
|
+
if (!canonicalInput) {
|
|
2516
|
+
return trimmed;
|
|
2517
|
+
}
|
|
2518
|
+
const resolvePreferred = (canonical) => {
|
|
2519
|
+
var _a;
|
|
2520
|
+
const definition = (_a = lookupBodySiteDefinition(customSiteMap, canonical)) !== null && _a !== void 0 ? _a : maps_1.DEFAULT_BODY_SITE_SNOMED[canonical];
|
|
2521
|
+
if (!definition) {
|
|
2522
|
+
return undefined;
|
|
2523
|
+
}
|
|
2524
|
+
const preferred = pickPreferredBodySitePhrase(canonical, definition, customSiteMap);
|
|
2525
|
+
const textValue = preferred !== null && preferred !== void 0 ? preferred : canonical;
|
|
2526
|
+
const normalized = (0, maps_1.normalizeBodySiteKey)(textValue);
|
|
2527
|
+
if (!normalized) {
|
|
2528
|
+
return undefined;
|
|
2529
|
+
}
|
|
2530
|
+
return { text: textValue, canonical: normalized };
|
|
2531
|
+
};
|
|
2532
|
+
if (isAdjectivalSitePhrase(canonicalInput)) {
|
|
2533
|
+
const direct = resolvePreferred(canonicalInput);
|
|
2534
|
+
return (_a = direct === null || direct === void 0 ? void 0 : direct.text) !== null && _a !== void 0 ? _a : trimmed;
|
|
2535
|
+
}
|
|
2536
|
+
const words = canonicalInput.split(/\s+/).filter((word) => word.length > 0);
|
|
2537
|
+
for (let i = 1; i < words.length; i++) {
|
|
2538
|
+
const prefix = words.slice(0, i);
|
|
2539
|
+
if (!prefix.every((word) => isAdjectivalSitePhrase(word))) {
|
|
2540
|
+
continue;
|
|
2541
|
+
}
|
|
2542
|
+
const candidateCanonical = words.slice(i).join(" ");
|
|
2543
|
+
if (!candidateCanonical) {
|
|
2544
|
+
continue;
|
|
2545
|
+
}
|
|
2546
|
+
const candidatePreferred = resolvePreferred(candidateCanonical);
|
|
2547
|
+
if (!candidatePreferred) {
|
|
2548
|
+
continue;
|
|
2549
|
+
}
|
|
2550
|
+
const prefixMatches = prefix.every((word) => {
|
|
2551
|
+
const normalizedPrefix = resolvePreferred(word);
|
|
2552
|
+
return (normalizedPrefix !== undefined &&
|
|
2553
|
+
normalizedPrefix.canonical === candidatePreferred.canonical);
|
|
2554
|
+
});
|
|
2555
|
+
if (!prefixMatches) {
|
|
2556
|
+
continue;
|
|
2557
|
+
}
|
|
2558
|
+
return candidatePreferred.text;
|
|
2559
|
+
}
|
|
2560
|
+
return trimmed;
|
|
2561
|
+
}
|
|
2562
|
+
function pickPreferredBodySitePhrase(canonical, definition, customSiteMap) {
|
|
2563
|
+
const synonyms = new Set();
|
|
2564
|
+
synonyms.add(canonical);
|
|
2565
|
+
if (definition.aliases) {
|
|
2566
|
+
for (const alias of definition.aliases) {
|
|
2567
|
+
const normalizedAlias = (0, maps_1.normalizeBodySiteKey)(alias);
|
|
2568
|
+
if (normalizedAlias) {
|
|
2569
|
+
synonyms.add(normalizedAlias);
|
|
2570
|
+
}
|
|
2571
|
+
}
|
|
2572
|
+
}
|
|
2573
|
+
const defaultSynonyms = DEFAULT_SITE_SYNONYM_KEYS.get(definition);
|
|
2574
|
+
if (defaultSynonyms) {
|
|
2575
|
+
for (const synonym of defaultSynonyms) {
|
|
2576
|
+
synonyms.add(synonym);
|
|
2577
|
+
}
|
|
2578
|
+
}
|
|
2579
|
+
if (customSiteMap) {
|
|
2580
|
+
for (const [key, candidate] of (0, object_1.objectEntries)(customSiteMap)) {
|
|
2581
|
+
if (!candidate) {
|
|
2582
|
+
continue;
|
|
2583
|
+
}
|
|
2584
|
+
if (candidate === definition) {
|
|
2585
|
+
const normalizedKey = (0, maps_1.normalizeBodySiteKey)(key);
|
|
2586
|
+
if (normalizedKey) {
|
|
2587
|
+
synonyms.add(normalizedKey);
|
|
2588
|
+
}
|
|
2589
|
+
if (candidate.aliases) {
|
|
2590
|
+
for (const alias of candidate.aliases) {
|
|
2591
|
+
const normalizedAlias = (0, maps_1.normalizeBodySiteKey)(alias);
|
|
2592
|
+
if (normalizedAlias) {
|
|
2593
|
+
synonyms.add(normalizedAlias);
|
|
2594
|
+
}
|
|
2595
|
+
}
|
|
2596
|
+
}
|
|
2597
|
+
}
|
|
2598
|
+
}
|
|
2599
|
+
}
|
|
2600
|
+
const candidates = Array.from(synonyms).filter((phrase) => phrase && !isAdjectivalSitePhrase(phrase));
|
|
2601
|
+
if (!candidates.length) {
|
|
2602
|
+
return undefined;
|
|
2603
|
+
}
|
|
2604
|
+
candidates.sort((a, b) => scoreBodySitePhrase(b) - scoreBodySitePhrase(a));
|
|
2605
|
+
const best = candidates[0];
|
|
2606
|
+
if (!best) {
|
|
2607
|
+
return undefined;
|
|
2608
|
+
}
|
|
2609
|
+
if ((0, maps_1.normalizeBodySiteKey)(best) === canonical) {
|
|
2610
|
+
return undefined;
|
|
2611
|
+
}
|
|
2612
|
+
return best;
|
|
2613
|
+
}
|
|
2614
|
+
function scoreBodySitePhrase(phrase) {
|
|
2615
|
+
const lower = phrase.toLowerCase();
|
|
2616
|
+
const words = lower.split(/\s+/).filter((part) => part.length > 0);
|
|
2617
|
+
let score = 0;
|
|
2618
|
+
if (!/(structure|region|entire|proper|body)/.test(lower)) {
|
|
2619
|
+
score += 3;
|
|
2620
|
+
}
|
|
2621
|
+
if (!lower.includes(" of ")) {
|
|
2622
|
+
score += 1;
|
|
2623
|
+
}
|
|
2624
|
+
if (words.length <= 2) {
|
|
2625
|
+
score += 1;
|
|
2626
|
+
}
|
|
2627
|
+
if (words.length === 1) {
|
|
2628
|
+
score += 0.5;
|
|
2629
|
+
}
|
|
2630
|
+
score -= words.length * 0.2;
|
|
2631
|
+
score -= lower.length * 0.01;
|
|
2632
|
+
return score;
|
|
2633
|
+
}
|
|
2634
|
+
function isAdjectivalSitePhrase(phrase) {
|
|
2635
|
+
const normalized = phrase.trim().toLowerCase();
|
|
2636
|
+
if (!normalized) {
|
|
2637
|
+
return false;
|
|
2638
|
+
}
|
|
2639
|
+
const words = normalized.split(/\s+/).filter((word) => word.length > 0);
|
|
2640
|
+
if (words.length !== 1) {
|
|
2641
|
+
return false;
|
|
2642
|
+
}
|
|
2643
|
+
const last = words[words.length - 1];
|
|
2644
|
+
if (last.length <= 3) {
|
|
2645
|
+
return false;
|
|
2646
|
+
}
|
|
2647
|
+
return BODY_SITE_ADJECTIVE_SUFFIXES.some((suffix) => last.endsWith(suffix));
|
|
2648
|
+
}
|
|
2402
2649
|
function collectAdditionalInstructions(internal, tokens) {
|
|
2403
2650
|
var _a, _b, _c, _d, _e, _f;
|
|
2404
2651
|
if (internal.additionalInstructions.length) {
|
|
2405
2652
|
return;
|
|
2406
2653
|
}
|
|
2407
|
-
const leftover = tokens.filter((token) => !internal.consumed.has(token.index));
|
|
2408
|
-
if (!leftover.length) {
|
|
2409
|
-
return;
|
|
2410
|
-
}
|
|
2411
2654
|
const punctuationOnly = /^[;:.,-]+$/;
|
|
2412
|
-
const
|
|
2413
|
-
|
|
2655
|
+
const trailing = [];
|
|
2656
|
+
let expectedIndex;
|
|
2657
|
+
for (let cursor = tokens.length - 1; cursor >= 0; cursor--) {
|
|
2658
|
+
const token = tokens[cursor];
|
|
2659
|
+
if (!token) {
|
|
2660
|
+
continue;
|
|
2661
|
+
}
|
|
2662
|
+
if (internal.consumed.has(token.index)) {
|
|
2663
|
+
if (trailing.length > 0) {
|
|
2664
|
+
break;
|
|
2665
|
+
}
|
|
2666
|
+
continue;
|
|
2667
|
+
}
|
|
2668
|
+
if (expectedIndex !== undefined && token.index !== expectedIndex - 1) {
|
|
2669
|
+
break;
|
|
2670
|
+
}
|
|
2671
|
+
trailing.unshift(token);
|
|
2672
|
+
expectedIndex = token.index;
|
|
2673
|
+
}
|
|
2674
|
+
if (!trailing.length) {
|
|
2414
2675
|
return;
|
|
2415
2676
|
}
|
|
2416
|
-
const
|
|
2417
|
-
|
|
2418
|
-
if (!contiguous) {
|
|
2677
|
+
const contentTokens = trailing.filter((token) => !punctuationOnly.test(token.original));
|
|
2678
|
+
if (!contentTokens.length) {
|
|
2419
2679
|
return;
|
|
2420
2680
|
}
|
|
2421
|
-
const
|
|
2681
|
+
const trailingIndices = trailing.map((token) => token.index).sort((a, b) => a - b);
|
|
2682
|
+
const lastIndex = trailingIndices[trailingIndices.length - 1];
|
|
2422
2683
|
for (let i = lastIndex + 1; i < tokens.length; i++) {
|
|
2423
|
-
const
|
|
2424
|
-
if (!
|
|
2684
|
+
const nextToken = tokens[i];
|
|
2685
|
+
if (!nextToken) {
|
|
2686
|
+
continue;
|
|
2687
|
+
}
|
|
2688
|
+
if (!internal.consumed.has(nextToken.index)) {
|
|
2425
2689
|
return;
|
|
2426
2690
|
}
|
|
2427
2691
|
}
|
|
@@ -2434,7 +2698,33 @@ function collectAdditionalInstructions(internal, tokens) {
|
|
|
2434
2698
|
return;
|
|
2435
2699
|
}
|
|
2436
2700
|
const contentIndices = contentTokens.map((token) => token.index).sort((a, b) => a - b);
|
|
2437
|
-
const
|
|
2701
|
+
const lowerInput = internal.input.toLowerCase();
|
|
2702
|
+
let trailingRange;
|
|
2703
|
+
let searchEnd = lowerInput.length;
|
|
2704
|
+
let rangeStart;
|
|
2705
|
+
let rangeEnd;
|
|
2706
|
+
for (let i = contentTokens.length - 1; i >= 0; i--) {
|
|
2707
|
+
const fragment = contentTokens[i].original.trim();
|
|
2708
|
+
if (!fragment) {
|
|
2709
|
+
continue;
|
|
2710
|
+
}
|
|
2711
|
+
const lowerFragment = fragment.toLowerCase();
|
|
2712
|
+
const foundIndex = lowerInput.lastIndexOf(lowerFragment, searchEnd - 1);
|
|
2713
|
+
if (foundIndex === -1) {
|
|
2714
|
+
rangeStart = undefined;
|
|
2715
|
+
rangeEnd = undefined;
|
|
2716
|
+
break;
|
|
2717
|
+
}
|
|
2718
|
+
rangeStart = foundIndex;
|
|
2719
|
+
if (rangeEnd === undefined) {
|
|
2720
|
+
rangeEnd = foundIndex + lowerFragment.length;
|
|
2721
|
+
}
|
|
2722
|
+
searchEnd = foundIndex;
|
|
2723
|
+
}
|
|
2724
|
+
if (rangeStart !== undefined && rangeEnd !== undefined) {
|
|
2725
|
+
trailingRange = { start: rangeStart, end: rangeEnd };
|
|
2726
|
+
}
|
|
2727
|
+
const range = trailingRange !== null && trailingRange !== void 0 ? trailingRange : computeTokenRange(internal.input, tokens, contentIndices);
|
|
2438
2728
|
let separatorDetected = false;
|
|
2439
2729
|
if (range) {
|
|
2440
2730
|
for (let cursor = range.start - 1; cursor >= 0; cursor--) {
|
|
@@ -2499,11 +2789,147 @@ function collectAdditionalInstructions(internal, tokens) {
|
|
|
2499
2789
|
}
|
|
2500
2790
|
if (instructions.length) {
|
|
2501
2791
|
internal.additionalInstructions = instructions;
|
|
2502
|
-
for (const token of
|
|
2792
|
+
for (const token of trailing) {
|
|
2503
2793
|
mark(internal.consumed, token);
|
|
2504
2794
|
}
|
|
2505
2795
|
}
|
|
2506
2796
|
}
|
|
2797
|
+
function determinePrnReasonCutoff(tokens, sourceText) {
|
|
2798
|
+
const separatorIndex = findPrnReasonSeparator(sourceText);
|
|
2799
|
+
if (separatorIndex === undefined) {
|
|
2800
|
+
return undefined;
|
|
2801
|
+
}
|
|
2802
|
+
const lowerSource = sourceText.toLowerCase();
|
|
2803
|
+
let searchOffset = 0;
|
|
2804
|
+
for (let i = 0; i < tokens.length; i++) {
|
|
2805
|
+
const token = tokens[i];
|
|
2806
|
+
const fragment = token.original.trim();
|
|
2807
|
+
if (!fragment) {
|
|
2808
|
+
continue;
|
|
2809
|
+
}
|
|
2810
|
+
const lowerFragment = fragment.toLowerCase();
|
|
2811
|
+
const position = lowerSource.indexOf(lowerFragment, searchOffset);
|
|
2812
|
+
if (position === -1) {
|
|
2813
|
+
continue;
|
|
2814
|
+
}
|
|
2815
|
+
const end = position + lowerFragment.length;
|
|
2816
|
+
searchOffset = end;
|
|
2817
|
+
if (position >= separatorIndex) {
|
|
2818
|
+
return i;
|
|
2819
|
+
}
|
|
2820
|
+
}
|
|
2821
|
+
return undefined;
|
|
2822
|
+
}
|
|
2823
|
+
function findPrnReasonSeparator(sourceText) {
|
|
2824
|
+
var _a;
|
|
2825
|
+
for (let i = 0; i < sourceText.length; i++) {
|
|
2826
|
+
const ch = sourceText[i];
|
|
2827
|
+
if (ch === "\n" || ch === "\r") {
|
|
2828
|
+
if (sourceText.slice(i + 1).trim().length > 0) {
|
|
2829
|
+
return i;
|
|
2830
|
+
}
|
|
2831
|
+
continue;
|
|
2832
|
+
}
|
|
2833
|
+
if (ch === ";") {
|
|
2834
|
+
if (sourceText.slice(i + 1).trim().length > 0) {
|
|
2835
|
+
return i;
|
|
2836
|
+
}
|
|
2837
|
+
continue;
|
|
2838
|
+
}
|
|
2839
|
+
if (ch === "-") {
|
|
2840
|
+
const prev = sourceText[i - 1];
|
|
2841
|
+
const next = sourceText[i + 1];
|
|
2842
|
+
const hasWhitespaceAround = (!prev || /\s/.test(prev)) && (!next || /\s/.test(next));
|
|
2843
|
+
if (hasWhitespaceAround && sourceText.slice(i + 1).trim().length > 0) {
|
|
2844
|
+
return i;
|
|
2845
|
+
}
|
|
2846
|
+
continue;
|
|
2847
|
+
}
|
|
2848
|
+
if (ch === ":" || ch === ".") {
|
|
2849
|
+
const rest = sourceText.slice(i + 1);
|
|
2850
|
+
if (!rest.trim().length) {
|
|
2851
|
+
continue;
|
|
2852
|
+
}
|
|
2853
|
+
const nextChar = rest.replace(/^\s+/, "")[0];
|
|
2854
|
+
if (!nextChar) {
|
|
2855
|
+
continue;
|
|
2856
|
+
}
|
|
2857
|
+
if (ch === "." &&
|
|
2858
|
+
/[0-9]/.test((_a = sourceText[i - 1]) !== null && _a !== void 0 ? _a : "") &&
|
|
2859
|
+
/[0-9]/.test(nextChar)) {
|
|
2860
|
+
continue;
|
|
2861
|
+
}
|
|
2862
|
+
return i;
|
|
2863
|
+
}
|
|
2864
|
+
}
|
|
2865
|
+
return undefined;
|
|
2866
|
+
}
|
|
2867
|
+
function findTrailingPrnSiteSuffix(tokens, internal, options) {
|
|
2868
|
+
var _a;
|
|
2869
|
+
let suffixStart;
|
|
2870
|
+
let hasSiteHint = false;
|
|
2871
|
+
let hasConnector = false;
|
|
2872
|
+
for (let i = tokens.length - 1; i >= 0; i--) {
|
|
2873
|
+
const token = tokens[i];
|
|
2874
|
+
const lower = normalizeTokenLower(token);
|
|
2875
|
+
if (!lower) {
|
|
2876
|
+
if (suffixStart !== undefined && token.original.trim()) {
|
|
2877
|
+
break;
|
|
2878
|
+
}
|
|
2879
|
+
continue;
|
|
2880
|
+
}
|
|
2881
|
+
if (isBodySiteHint(lower, internal.customSiteHints)) {
|
|
2882
|
+
hasSiteHint = true;
|
|
2883
|
+
suffixStart = i;
|
|
2884
|
+
continue;
|
|
2885
|
+
}
|
|
2886
|
+
if (suffixStart !== undefined) {
|
|
2887
|
+
if (SITE_CONNECTORS.has(lower)) {
|
|
2888
|
+
hasConnector = true;
|
|
2889
|
+
suffixStart = i;
|
|
2890
|
+
continue;
|
|
2891
|
+
}
|
|
2892
|
+
if (SITE_FILLER_WORDS.has(lower) || ROUTE_DESCRIPTOR_FILLER_WORDS.has(lower)) {
|
|
2893
|
+
suffixStart = i;
|
|
2894
|
+
continue;
|
|
2895
|
+
}
|
|
2896
|
+
}
|
|
2897
|
+
if (suffixStart !== undefined) {
|
|
2898
|
+
break;
|
|
2899
|
+
}
|
|
2900
|
+
}
|
|
2901
|
+
if (!hasSiteHint || !hasConnector || suffixStart === undefined || suffixStart === 0) {
|
|
2902
|
+
return undefined;
|
|
2903
|
+
}
|
|
2904
|
+
const suffixTokens = tokens.slice(suffixStart);
|
|
2905
|
+
const siteWords = [];
|
|
2906
|
+
for (const token of suffixTokens) {
|
|
2907
|
+
const trimmed = token.original.trim();
|
|
2908
|
+
if (!trimmed) {
|
|
2909
|
+
continue;
|
|
2910
|
+
}
|
|
2911
|
+
const lower = normalizeTokenLower(token);
|
|
2912
|
+
if (SITE_CONNECTORS.has(lower) ||
|
|
2913
|
+
SITE_FILLER_WORDS.has(lower) ||
|
|
2914
|
+
ROUTE_DESCRIPTOR_FILLER_WORDS.has(lower)) {
|
|
2915
|
+
continue;
|
|
2916
|
+
}
|
|
2917
|
+
siteWords.push(trimmed);
|
|
2918
|
+
}
|
|
2919
|
+
if (!siteWords.length) {
|
|
2920
|
+
return undefined;
|
|
2921
|
+
}
|
|
2922
|
+
const sitePhrase = siteWords.join(" ");
|
|
2923
|
+
const canonical = (0, maps_1.normalizeBodySiteKey)(sitePhrase);
|
|
2924
|
+
if (!canonical) {
|
|
2925
|
+
return undefined;
|
|
2926
|
+
}
|
|
2927
|
+
const definition = (_a = lookupBodySiteDefinition(options === null || options === void 0 ? void 0 : options.siteCodeMap, canonical)) !== null && _a !== void 0 ? _a : maps_1.DEFAULT_BODY_SITE_SNOMED[canonical];
|
|
2928
|
+
if (!definition) {
|
|
2929
|
+
return undefined;
|
|
2930
|
+
}
|
|
2931
|
+
return suffixStart;
|
|
2932
|
+
}
|
|
2507
2933
|
function lookupPrnReasonDefinition(map, canonical) {
|
|
2508
2934
|
if (!map) {
|
|
2509
2935
|
return undefined;
|