deterministic-location.js — extracción conservadora de ubicaciones

Overview

Este módulo implementa la extracción determinista de ubicaciones como texto para NLU-02A. La lógica es conservadora: no geocodifica, no usa catálogo geográfico y prefiere no extraer antes que devolver un lugar incorrecto .

El resultado separa dos salidas: places, para fragmentos que sí nombran un lugar, y unresolved, para evidencia textual que parece ubicación, pero no cumple las reglas. Las pruebas confirman que el módulo preserva fragmentos exactos, admite caracteres compuestos y conserva frases largas como evidencia no resuelta .

[!IMPORTANT] Este archivo solo extrae fragmentos textuales. No valida geografía, no consulta servicios externos y no resuelve lugares por sí mismo .

How it works

flowchart TD
  Text[Texto original] --> Extract[extractLocations]
  Lexicon[Coincidencias del lexico] --> Extract
  Extract --> Tokenize[tokenize]
  Tokenize --> Scan[Recorrido de tokens]
  Scan --> EnRule[Regla en]
  Scan --> CercaRule[Regla cerca de]
  Scan --> CueRule[Regla location cue]
  EnRule --> Collect[collectPlace]
  CercaRule --> Collect
  CueRule --> Collect
  Collect --> Trim[Recorte de conectores y cues]
  Trim --> Classify[Clasificar fragmento]
  Classify --> Places[places]
  Classify --> Unresolved[unresolved]

extractLocations recorre el texto tokenizado y consulta las coincidencias del léxico. Luego aplica tres disparadores principales: en, cerca de y las señales de ubicación del léxico. Cada candidato pasa por collectPlace, que recorta conectores finales, elimina algunos inicios direccionales y decide si el fragmento es un lugar confirmado o solo evidencia .

El diseño también evita solapamientos. Si un fragmento ya quedó registrado, otro candidato que lo cruce se descarta. Al final, ambas listas se ordenan por posición de aparición.

Key components

{
    "title": "extractLocations",
    "content": "Funci\u00f3n [badge:public] que recibe texto y coincidencias del l\u00e9xico. Devuelve ubicaciones confirmadas y evidencia no resuelta.",
    "type": "function",
    "filePath": "services/api/src/modules/interpretation/deterministic-location.js",
    "badges": [
        "public"
    ]
}
{
    "title": "collectPlace",
    "content": "Funci\u00f3n interna que agrupa el fragmento candidato, recorta conectores y decide si el texto nombra un lugar.",
    "type": "function",
    "filePath": "services/api/src/modules/interpretation/deterministic-location.js",
    "badges": []
}
{
    "title": "tokenize",
    "content": "Funci\u00f3n interna que divide el texto en tokens con posici\u00f3n, tipo y forma normalizada.",
    "type": "function",
    "filePath": "services/api/src/modules/interpretation/deterministic-location.js",
    "badges": []
}
{
    "title": "coveringMatch",
    "content": "Funci\u00f3n interna que encuentra una coincidencia del l\u00e9xico que cruza un rango de texto.",
    "type": "function",
    "filePath": "services/api/src/modules/interpretation/deterministic-location.js",
    "badges": []
}
{
    "title": "isLocationCue",
    "content": "Funci\u00f3n interna que reconoce se\u00f1ales del l\u00e9xico marcadas como locationCue y no ambiguas.",
    "type": "function",
    "filePath": "services/api/src/modules/interpretation/deterministic-location.js",
    "badges": []
}
{
    "title": "isDirectionalCue",
    "content": "Funci\u00f3n interna que detecta expresiones direccionales como rumbo a y por el rumbo de.",
    "type": "function",
    "filePath": "services/api/src/modules/interpretation/deterministic-location.js",
    "badges": []
}

Data & fields

Este módulo trabaja con tokens, coincidencias del léxico y dos salidas principales. La estructura devuelta por extractLocations es estable y ordenada por aparición. Los campos conservan el fragmento exacto del texto original.

[
    {
        "name": "text",
        "type": "string",
        "required": true,
        "description": "Texto original que se analiza."
    },
    {
        "name": "matches",
        "type": "Array<object>",
        "required": true,
        "description": "Coincidencias producidas por findLexiconMatches(text). Este módulo usa span, matched, entryId, ambiguous y facets."
    }
]
[
    {
        "name": "places",
        "type": "Array<object>",
        "required": true,
        "description": "Fragmentos confirmados como lugares, en orden de aparición.",
        "children": [
            {
                "name": "value",
                "type": "string",
                "required": true,
                "description": "Fragmento exacto tomado del texto."
            },
            {
                "name": "span",
                "type": "[number, number]",
                "required": true,
                "description": "Rango UTF-16 del fragmento en el texto original."
            },
            {
                "name": "matched",
                "type": "string",
                "required": true,
                "description": "Copia exacta del mismo fragmento."
            },
            {
                "name": "rule",
                "type": "string",
                "required": true,
                "description": "Regla que activó la extracción."
            }
        ]
    },
    {
        "name": "unresolved",
        "type": "Array<object>",
        "required": true,
        "description": "Fragmentos conservados como evidencia no resuelta.",
        "children": [
            {
                "name": "span",
                "type": "[number, number]",
                "required": true,
                "description": "Rango UTF-16 del fragmento conservado."
            },
            {
                "name": "matched",
                "type": "string",
                "required": true,
                "description": "Fragmento exacto que no pudo confirmarse como lugar."
            }
        ]
    }
]
  • MAX_LOCATION_WORDS [badge:read-only] number — límite de palabras por fragmento. El valor es 8.
  • MAX_LOCATION_LENGTH [badge:read-only] number — límite de longitud del fragmento. El valor es 200.
  • TOKEN [badge:read-only] RegExp — separa palabras, números y signos.
  • WORD [badge:read-only] RegExp — identifica tokens que empiezan con letra.
  • CONNECTORS [badge:read-only] Set<string> — artículos y conectores que pueden vivir dentro del nombre.
  • STOP_WORDS [badge:read-only] Set<string> — palabras que cierran el fragmento.
  • NOT_A_PLACE [badge:read-only] Set<string> — términos que describen, fechan o generalizan, pero no nombran un lugar.
  • DIRECTIONAL_CUES [badge:read-only] Set<string> — expresiones direccionales que se recortan del inicio.

Este conjunto agrupa palabras que el módulo interpreta como evidencia no geográfica.

  • Fechas, meses, días y temporadas.
  • Descripciones de zona o calidad.
  • Pronombres, determinantes y palabras genéricas.
  • Términos de estado, tiempo, dinero y proceso.
  • Palabras que suelen describir contexto, no ubicación.

Behaviors

Cada función cumple una responsabilidad pequeña y predecible. La combinación de todas produce una extracción conservadora y reproducible.

  • tokenize(text) — devuelve una secuencia de tokens con text, start, end, isWord y normalized.
  • coveringMatch(matches, start, end) — devuelve la primera coincidencia del léxico que cruza un rango dado, o undefined.
  • isLocationCue(match) — devuelve true cuando la coincidencia no es ambigua y contiene el facet locationCue.
  • isDirectionalCue(match) — devuelve true para rumbo a y por el rumbo de.
  • collectPlace(tokens, index, matches) — construye un candidato de ubicación desde un token inicial y devuelve { start, end, place } o null.
  • extractLocations(text, matches) — devuelve { places, unresolved }, con ambas listas ordenadas por aparición.

La decisión final en collectPlace depende de tres condiciones. Primero, necesita palabras con contenido. Segundo, debe respetar los límites de longitud. Tercero, la primera palabra con contenido no debe pertenecer a NOT_A_PLACE.

[!NOTE] extractLocations no trunca frases largas. Si el fragmento supera los límites, se conserva en unresolved como evidencia no resuelta .

Gotchas & edge cases

[!WARNING] Si text no es una cadena, o está vacío, la función devuelve listas vacías. No lanza error por ese caso.

[!WARNING] Un fragmento puede parecer ubicación y terminar en unresolved. Eso ocurre si empieza con una descripción, una fecha o un término no geográfico.

[!CAUTION] en solo dispara extracción cuando no forma parte de una coincidencia del léxico. El comentario del archivo pone como ejemplo en renta, que no debe leerse como lugar .

[!WARNING] cerca de tiene tratamiento especial solo cuando el léxico lo marca como ambiguous.cerca_de. En ese caso, el fragmento útil empieza en la palabra siguiente.

[!NOTE] Las señales de ubicación del léxico forman parte del fragmento. En cambio, las expresiones direccionales iniciales se eliminan del resultado.

[!WARNING] Los resultados no se solapan. Si un fragmento ya quedó registrado, otro candidato que lo cruce se ignora.

services/api/src/modules/interpretation/deterministic-interpreter.js importa este módulo y lo combina con findLexiconMatches y extractUpperBoundBudgets para construir el resultado determinista general .

services/api/src/modules/interpretation/real-estate-lexicon.js provee las coincidencias que este archivo consume. Ese léxico define las señales de ubicación y los casos ambiguos que activan reglas como locationCue y ambiguous.cerca_de .

services/api/src/modules/interpretation/text-normalization.js aporta normalizeTerm, que este módulo usa para comparar tokens y frases sin depender de mayúsculas ni variantes simples de escritura.

services/api/test/interpretation-deterministic-units.test.js valida el comportamiento de esta extracción. Las pruebas cubren rechazo conservador, fragmentos exactos, caracteres compuestos, y el recorte de conectores finales .

services/api/src/modules/interpretation/interpretation-contract.js define el contrato general de interpretación que el intérprete determinista debe respetar en ejecución. Este archivo alimenta esa cadena con fragmentos textuales consistentes y provenientes del texto original .