El bug llevaba en producción más de un mes. Estaba en uno de los archivos que más confianza nos inspiraba — el script que extrae los trazados oficiales de la Xunta y los convierte en GeoJSON para nuestro visor de mapas. Llevaba semanas funcionando bien para todo el Camino Portugués y todo el Inglés. Pero cuando re-corrí la auditoría tras integrar el Camino Francés en su tramo gallego, una etapa apareció en rojo: Pontevedra → Caldas de Reis, salto de 5.494 metros.
5.494 metros. En una etapa de 22 km. El motor cartográfico estaba saltándose un tramo de cinco kilómetros y medio entre dos puntos consecutivos del trazado oficial. Si alguien hubiera bajado ese GPX a su GPS y hubiera empezado a caminar, habría visto cómo en mitad del bosque la línea desaparece y reaparece a las afueras de Caldas de Reis. La incidencia no estaba en producción visible — todavía no habíamos integrado esa etapa — pero estaba un commit antes de estarlo.
Cómo aparece un fantasma de 5 kilómetros
El KML que publica la Xunta de Galicia es un archivo XML enorme con la geometría de todos los Caminos jacobeos. Para cada etapa, el archivo agrupa varios placemarks — fragmentos de LineString con miles de coordenadas — bajo nombres como CPO-e04t01, CPO-e04t02, etcétera. CPO es Camino Portugués, e04 es etapa cuatro, t01 es tramo uno.
El detalle interesante: a veces la Xunta subdivide un tramo en sub-segmentos numerados. En lugar de un único CPO-e04t03 que cubra todo el tramo tres de la etapa cuatro, hay tres placemarks: CPO-e04t03p1, CPO-e04t03p2, CPO-e04t03p3. Cada uno cubre una parte del recorrido. Para reconstruir el tramo entero hay que concatenarlos en orden.
El integrador filtraba los placemarks con una regex que, simplificada, decía: «empieza con el código del camino, después e + número de etapa + t + número de tramo. Si después hay una letra v o c es una variante (descártalo). Si hay pN es un sub-segmento numerado (acéptalo)».
El bug estaba en el orden de los dos últimos grupos.
El detalle de cinco caracteres
La regex era esta:
^CPO-eNNt(\d+)([a-zA-Z]?)(p\d+)?
Léela despacio. El motor de expresiones regulares es greedy: intenta consumir todo lo que pueda en el primer grupo opcional disponible. Cuando ve la cadena CPO-e04t03p1:
- Matchea
CPO-e04tliteral. - Matchea
03con(\d+). - Ahora viene
([a-zA-Z]?). La siguiente letra esp. El motor la consume. - Llega a
(p\d+)?con la cadena restante1. No matchea (necesita unapque ya ha sido tragada).
Resultado: la p queda registrada como «sufijo letra» (que el filtro asume que es una variante alternativa tipo v o c) y el placemark se descarta. Los tres sub-segmentos del tramo tres de la etapa cuatro nunca entran en el GeoJSON final. La línea final pasa directamente del final del tramo dos al inicio del tramo cuatro, dejando un agujero de 5.494 metros donde debería haber pasado el sendero por el albergue de Barro.
El arreglo es invertir el orden de los grupos:
^CPO-eNNt(\d+)(p\d+)?([a-zA-Z]?)
Ahora el motor primero intenta consumir el p\d+ entero, y solo si no lo consigue cae al sufijo letra. Cinco caracteres en total. La línea reconstruida pasó de 1.260 puntos a 1.755 puntos, y el salto bajó a 76 metros. La etapa publicada ahora cubre los 22,9 km reales en vez de los 22,1 km del bug.
Lo incómodo
El integrador llevaba semanas dando resultados visiblemente correctos para el Portugués: cinco de las seis etapas no tenían subdivisión de tramos, y por tanto la regex nunca llegaba al caso conflictivo. Solo la cuarta etapa lo sufría, y el salto era invisible hasta que comparamos contra la auditoría de saltos máximos por trazado. Si no hubiéramos tenido esa auditoría previa funcionando, el error habría llegado a producción tal cual.
La lección no es la regex en sí — esos errores los comete cualquiera. La lección es lo lejos que está la confianza de la corrección. Confiábamos en el integrador porque «funcionaba en el 90 % de los casos» y los placemarks generados se veían bien sobre el mapa. Lo que no se ve en un mapa de zoom medio es un agujero de cinco kilómetros entre dos puntos consecutivos. El visor traza una línea recta entre ellos y ofrece una bonita ficción de continuidad.
Lo que cambió después
Tres decisiones tras el incidente.
Una. Auditoría de saltos como control obligatorio antes de publicar cualquier trazado, no como herramienta opcional. El umbral es 500 metros entre puntos consecutivos. Por encima de eso, el track no se publica hasta que se entienda qué causa el salto.
Dos. Cuando integramos el Camino Norte y el Primitivo desde sus KML Xunta, descubrimos que la nomenclatura es aún menos fiable. Hay placemarks que se llaman tNN entero junto a placemarks tNNpX que cubren parcialmente la misma sección. La heurística por nombre no podía decidir cuál usar y cuál ignorar. Reescribimos el integrador para usar conectividad geográfica: ordenar los placemarks por proximidad del extremo final de uno al extremo inicial del siguiente, con un umbral de 800 metros. Si un placemark no se puede conectar al anterior por debajo de ese umbral, queda fuera.
Tres. Esta entrada. La publicamos porque el error es genérico y útil para cualquiera que escriba parsers de cartografía oficial. La Xunta no inventa la nomenclatura para confundirnos: hereda una convención de un sistema más antiguo. Si te encuentras integrando datos abiertos de cualquier administración española, la probabilidad de que un detalle análogo te muerda es real.
Cifras finales del incidente
Tiempo total de bug en producción: 4-5 semanas (desde la integración del Portugués hasta el audit que lo detectó).
Etapas afectadas con salto >500 m por la misma raíz: 1 (Pontevedra-Caldas de Reis).
Etapas afectadas con sub-segmentos que también se descartaban pero sin generar salto visible: 0 confirmadas, posiblemente más en KMLs aún no integrados.
Caracteres modificados en el fix: 5.
Tiempo de redacción de esta entrada: 90 minutos.
La proporción 90 minutos de prosa por 5 caracteres de código es lo que diferencia un cuaderno de un changelog. Si el bug solo importa para el que lo escribió, no merece publicarse. Si la explicación de por qué pasó importa a quien viene después, sí.
Cristian Corrales
· Tras integración Francés gallego H27