@precisa-saude/fhir-ocr-utils 0.38.0 → 0.38.1

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
@@ -90,9 +90,13 @@ Por isso a correspondência é conservadora:
90
90
  `RBC_Urine`, "PH" ancora `pH_Urine`, e "COLOR", "KETONES", "PROTEIN" e
91
91
  companhia ancoram mesmo sem valor na linha (texto em colunas). O código do
92
92
  sangue não ancora pela mesma linha. A seção acaba na primeira linha com
93
- exame de outro painel ou na primeira linha sem valor e sem nome conhecido
94
- (o jeito de um cabeçalho); na dúvida ela acaba, e o nome volta ao sentido de
95
- fora da seção. Fora dela nada muda.
93
+ exame de outro painel, no cabeçalho de outro painel ou no fim do texto. Uma
94
+ linha sem valor e sem nome conhecido pode ser as duas coisas ("COMPREHENSIVE
95
+ METABOLIC PANEL" ou "MUCUS"), e quem decide é a próxima linha decisiva: se só
96
+ pode ser da urina (código da urina, a palavra urina, ou /HPF e /LPF), a seção
97
+ segue; se é exame de outro painel, ou se o texto acaba sem nada decisivo, a
98
+ seção acaba ali, e o nome volta ao sentido de fora da seção. Fora dela nada
99
+ muda.
96
100
  - **Contexto genético é descartado** — símbolos de gene colidem com nomes de
97
101
  biomarcador (o gene `APOB` vs. a lipoproteína `ApoB`). Linhas com acesso
98
102
  RefSeq (`NM_000384.2`), notação HGVS (`p.Trp448*`, `c.1234A>G`), `rs` do dbSNP
package/dist/cli.js CHANGED
@@ -135,6 +135,34 @@ var QUALITATIVE_VALUE_TERMS = /* @__PURE__ */ new Set([
135
135
  "undetectable",
136
136
  "yellow"
137
137
  ]);
138
+ var GENETIC_CONTEXT_PATTERNS = [
139
+ /\b[nx][mrpc]_\d{6,}/,
140
+ // RefSeq: NM_000384.2, NP_, NR_, XM_
141
+ /\bens[gtp]\d{6,}/,
142
+ // Ensembl: ENSG00000084674
143
+ /\bp\.[a-z]{3}\d/,
144
+ // HGVS proteína: p.Trp448*
145
+ /\bc\.\d+[acgt]?[>_+-]/,
146
+ // HGVS codificante: c.1234A>G, c.76_78del
147
+ /\brs\d{4,}\b/,
148
+ // dbSNP
149
+ /\bgenes?\b/,
150
+ /\bvariante?s?\b/,
151
+ /\bexons?\b/,
152
+ /\bzygosity\b/,
153
+ /\bzigosidade\b/,
154
+ /\balleles?\b/,
155
+ /\balelos?\b/,
156
+ /\bmutations?\b/,
157
+ /\bmutac(ao|oes)\b/,
158
+ /\bpathogenic/,
159
+ /\bpatogenic/,
160
+ /\bheterozyg/,
161
+ /\bhomozyg/,
162
+ /\bheterozigot/,
163
+ /\bhomozigot/,
164
+ /\bsequence change\b/
165
+ ];
138
166
 
139
167
  // src/body-region.ts
140
168
  var WHOLE_BODY_COMPOSITION_CODES = /* @__PURE__ */ new Set([
@@ -283,19 +311,37 @@ function isSectionName(matched) {
283
311
  return URINALYSIS_SECTION_NAMES.has(matched) || URINALYSIS_SECTION_NAMES.has(matched.replace(/s$/, ""));
284
312
  }
285
313
  var MENTIONS_URINE = /(?<![\p{L}\p{N}])urin[ae](?![\p{L}\p{N}])/u;
314
+ var SEDIMENT_FIELD_UNIT = /\/(?:hpf|lpf)(?![\p{L}\p{N}])/u;
315
+ function hasUrineCue(line) {
316
+ return MENTIONS_URINE.test(line) || SEDIMENT_FIELD_UNIT.test(line);
317
+ }
318
+ function kindOf(line) {
319
+ const text = line.text.trim();
320
+ if (!text) return "blank";
321
+ if (URINALYSIS_HEADER.test(text) && !/\d/.test(text)) return "header";
322
+ if (line.foreign) return "foreign";
323
+ if (line.urine) return "urine";
324
+ return line.known || line.hasValue ? "neutral" : "unknown";
325
+ }
286
326
  function urinalysisLineIndexes(lines) {
327
+ const kinds = lines.map(kindOf);
328
+ const continuesAsUrine = (from) => {
329
+ const next = kinds.findIndex(
330
+ (kind, index) => index > from && (kind === "urine" || kind === "foreign" || kind === "header")
331
+ );
332
+ return next !== -1 && kinds[next] === "urine";
333
+ };
287
334
  const inside = /* @__PURE__ */ new Set();
288
335
  let open = false;
289
- lines.forEach((line, index) => {
290
- const text = line.text.trim();
291
- if (URINALYSIS_HEADER.test(text) && !/\d/.test(text)) {
336
+ kinds.forEach((kind, index) => {
337
+ if (kind === "header") {
292
338
  open = true;
293
339
  return;
294
340
  }
295
- if (!open || !text) {
341
+ if (!open || kind === "blank") {
296
342
  return;
297
343
  }
298
- if (line.foreign || !line.known && !line.hasValue && !MENTIONS_URINE.test(text)) {
344
+ if (kind === "foreign" || kind === "unknown" && !continuesAsUrine(index)) {
299
345
  open = false;
300
346
  return;
301
347
  }
@@ -339,7 +385,7 @@ function applyUrinalysisSection(normalizedText, candidates, deps) {
339
385
  const known = deps.patterns.some(
340
386
  ({ name, regex }) => !matchesIn(text, name, regex).next().done
341
387
  );
342
- return { foreign: false, hasValue: deps.hasValue(text), known, text };
388
+ return { foreign: false, hasValue: deps.hasValue(text), known, text, urine: hasUrineCue(text) };
343
389
  });
344
390
  for (const candidate of resolved) {
345
391
  let index = lineStarts.length - 1;
@@ -347,7 +393,9 @@ function applyUrinalysisSection(normalizedText, candidates, deps) {
347
393
  const line = lines[index];
348
394
  line.known = true;
349
395
  const matched = normalizedText.slice(candidate.start, candidate.end);
350
- if (!isSectionName(matched) && !candidate.entries.some((e) => deps.urineCodes.has(e.code))) {
396
+ if (candidate.entries.some((e) => deps.urineCodes.has(e.code))) {
397
+ line.urine = true;
398
+ } else if (!isSectionName(matched)) {
351
399
  line.foreign = true;
352
400
  }
353
401
  }
@@ -420,40 +468,13 @@ function foldCommas(text) {
420
468
  (comma, space, offset) => startsWithCatalogName(text.slice(offset + 1 + space.length)) ? comma : " "
421
469
  );
422
470
  }
423
- var GENETIC_CONTEXT_PATTERNS = [
424
- /\b[nx][mrpc]_\d{6,}/,
425
- // RefSeq: NM_000384.2, NP_, NR_, XM_
426
- /\bens[gtp]\d{6,}/,
427
- // Ensembl: ENSG00000084674
428
- /\bp\.[a-z]{3}\d/,
429
- // HGVS proteína: p.Trp448*
430
- /\bc\.\d+[acgt]?[>_+-]/,
431
- // HGVS codificante: c.1234A>G, c.76_78del
432
- /\brs\d{4,}\b/,
433
- // dbSNP
434
- /\bgenes?\b/,
435
- /\bvariante?s?\b/,
436
- /\bexons?\b/,
437
- /\bzygosity\b/,
438
- /\bzigosidade\b/,
439
- /\balleles?\b/,
440
- /\balelos?\b/,
441
- /\bmutations?\b/,
442
- /\bmutac(ao|oes)\b/,
443
- /\bpathogenic/,
444
- /\bpatogenic/,
445
- /\bheterozyg/,
446
- /\bhomozyg/,
447
- /\bheterozigot/,
448
- /\bhomozigot/,
449
- /\bsequence change\b/
450
- ];
451
471
  var DIGIT_PATTERN = /\d/;
452
472
  var cachedUnitTokens = null;
453
473
  function getUnitTokens() {
454
474
  if (!cachedUnitTokens) {
475
+ const isExamName = (t) => URINALYSIS_SECTION_NAMES.has(t) || getNamePatterns().has(t);
455
476
  cachedUnitTokens = new Set(
456
- Object.keys(UNIT_TO_UCUM).map((unit) => normalize(unit).trim()).filter(Boolean)
477
+ Object.keys(UNIT_TO_UCUM).map((unit) => normalize(unit).trim()).filter((unit) => unit && !isExamName(unit))
457
478
  );
458
479
  }
459
480
  return cachedUnitTokens;
@@ -1218,7 +1239,7 @@ async function main() {
1218
1239
  strict: false
1219
1240
  });
1220
1241
  if (values.version) {
1221
- process.stdout.write(`${"0.38.0"}
1242
+ process.stdout.write(`${"0.38.1"}
1222
1243
  `);
1223
1244
  return;
1224
1245
  }
package/dist/index.cjs CHANGED
@@ -119,6 +119,34 @@ var QUALITATIVE_VALUE_TERMS = /* @__PURE__ */ new Set([
119
119
  "undetectable",
120
120
  "yellow"
121
121
  ]);
122
+ var GENETIC_CONTEXT_PATTERNS = [
123
+ /\b[nx][mrpc]_\d{6,}/,
124
+ // RefSeq: NM_000384.2, NP_, NR_, XM_
125
+ /\bens[gtp]\d{6,}/,
126
+ // Ensembl: ENSG00000084674
127
+ /\bp\.[a-z]{3}\d/,
128
+ // HGVS proteína: p.Trp448*
129
+ /\bc\.\d+[acgt]?[>_+-]/,
130
+ // HGVS codificante: c.1234A>G, c.76_78del
131
+ /\brs\d{4,}\b/,
132
+ // dbSNP
133
+ /\bgenes?\b/,
134
+ /\bvariante?s?\b/,
135
+ /\bexons?\b/,
136
+ /\bzygosity\b/,
137
+ /\bzigosidade\b/,
138
+ /\balleles?\b/,
139
+ /\balelos?\b/,
140
+ /\bmutations?\b/,
141
+ /\bmutac(ao|oes)\b/,
142
+ /\bpathogenic/,
143
+ /\bpatogenic/,
144
+ /\bheterozyg/,
145
+ /\bhomozyg/,
146
+ /\bheterozigot/,
147
+ /\bhomozigot/,
148
+ /\bsequence change\b/
149
+ ];
122
150
 
123
151
  // src/body-region.ts
124
152
  var WHOLE_BODY_COMPOSITION_CODES = /* @__PURE__ */ new Set([
@@ -267,19 +295,37 @@ function isSectionName(matched) {
267
295
  return URINALYSIS_SECTION_NAMES.has(matched) || URINALYSIS_SECTION_NAMES.has(matched.replace(/s$/, ""));
268
296
  }
269
297
  var MENTIONS_URINE = /(?<![\p{L}\p{N}])urin[ae](?![\p{L}\p{N}])/u;
298
+ var SEDIMENT_FIELD_UNIT = /\/(?:hpf|lpf)(?![\p{L}\p{N}])/u;
299
+ function hasUrineCue(line) {
300
+ return MENTIONS_URINE.test(line) || SEDIMENT_FIELD_UNIT.test(line);
301
+ }
302
+ function kindOf(line) {
303
+ const text = line.text.trim();
304
+ if (!text) return "blank";
305
+ if (URINALYSIS_HEADER.test(text) && !/\d/.test(text)) return "header";
306
+ if (line.foreign) return "foreign";
307
+ if (line.urine) return "urine";
308
+ return line.known || line.hasValue ? "neutral" : "unknown";
309
+ }
270
310
  function urinalysisLineIndexes(lines) {
311
+ const kinds = lines.map(kindOf);
312
+ const continuesAsUrine = (from) => {
313
+ const next = kinds.findIndex(
314
+ (kind, index) => index > from && (kind === "urine" || kind === "foreign" || kind === "header")
315
+ );
316
+ return next !== -1 && kinds[next] === "urine";
317
+ };
271
318
  const inside = /* @__PURE__ */ new Set();
272
319
  let open = false;
273
- lines.forEach((line, index) => {
274
- const text = line.text.trim();
275
- if (URINALYSIS_HEADER.test(text) && !/\d/.test(text)) {
320
+ kinds.forEach((kind, index) => {
321
+ if (kind === "header") {
276
322
  open = true;
277
323
  return;
278
324
  }
279
- if (!open || !text) {
325
+ if (!open || kind === "blank") {
280
326
  return;
281
327
  }
282
- if (line.foreign || !line.known && !line.hasValue && !MENTIONS_URINE.test(text)) {
328
+ if (kind === "foreign" || kind === "unknown" && !continuesAsUrine(index)) {
283
329
  open = false;
284
330
  return;
285
331
  }
@@ -323,7 +369,7 @@ function applyUrinalysisSection(normalizedText, candidates, deps) {
323
369
  const known = deps.patterns.some(
324
370
  ({ name, regex }) => !matchesIn(text, name, regex).next().done
325
371
  );
326
- return { foreign: false, hasValue: deps.hasValue(text), known, text };
372
+ return { foreign: false, hasValue: deps.hasValue(text), known, text, urine: hasUrineCue(text) };
327
373
  });
328
374
  for (const candidate of resolved) {
329
375
  let index = lineStarts.length - 1;
@@ -331,7 +377,9 @@ function applyUrinalysisSection(normalizedText, candidates, deps) {
331
377
  const line = lines[index];
332
378
  line.known = true;
333
379
  const matched = normalizedText.slice(candidate.start, candidate.end);
334
- if (!isSectionName(matched) && !candidate.entries.some((e) => deps.urineCodes.has(e.code))) {
380
+ if (candidate.entries.some((e) => deps.urineCodes.has(e.code))) {
381
+ line.urine = true;
382
+ } else if (!isSectionName(matched)) {
335
383
  line.foreign = true;
336
384
  }
337
385
  }
@@ -404,40 +452,13 @@ function foldCommas(text) {
404
452
  (comma, space, offset) => startsWithCatalogName(text.slice(offset + 1 + space.length)) ? comma : " "
405
453
  );
406
454
  }
407
- var GENETIC_CONTEXT_PATTERNS = [
408
- /\b[nx][mrpc]_\d{6,}/,
409
- // RefSeq: NM_000384.2, NP_, NR_, XM_
410
- /\bens[gtp]\d{6,}/,
411
- // Ensembl: ENSG00000084674
412
- /\bp\.[a-z]{3}\d/,
413
- // HGVS proteína: p.Trp448*
414
- /\bc\.\d+[acgt]?[>_+-]/,
415
- // HGVS codificante: c.1234A>G, c.76_78del
416
- /\brs\d{4,}\b/,
417
- // dbSNP
418
- /\bgenes?\b/,
419
- /\bvariante?s?\b/,
420
- /\bexons?\b/,
421
- /\bzygosity\b/,
422
- /\bzigosidade\b/,
423
- /\balleles?\b/,
424
- /\balelos?\b/,
425
- /\bmutations?\b/,
426
- /\bmutac(ao|oes)\b/,
427
- /\bpathogenic/,
428
- /\bpatogenic/,
429
- /\bheterozyg/,
430
- /\bhomozyg/,
431
- /\bheterozigot/,
432
- /\bhomozigot/,
433
- /\bsequence change\b/
434
- ];
435
455
  var DIGIT_PATTERN = /\d/;
436
456
  var cachedUnitTokens = null;
437
457
  function getUnitTokens() {
438
458
  if (!cachedUnitTokens) {
459
+ const isExamName = (t) => URINALYSIS_SECTION_NAMES.has(t) || getNamePatterns().has(t);
439
460
  cachedUnitTokens = new Set(
440
- Object.keys(_fhir.UNIT_TO_UCUM).map((unit) => normalize(unit).trim()).filter(Boolean)
461
+ Object.keys(_fhir.UNIT_TO_UCUM).map((unit) => normalize(unit).trim()).filter((unit) => unit && !isExamName(unit))
441
462
  );
442
463
  }
443
464
  return cachedUnitTokens;