Acentos incorrectos en un Shapefile: .cpg, Latin-1 y UTF-8
Respuesta corta: el .dbf que guarda los atributos de un Shapefile no tiene codificación propia. Un sidecar de pocos bytes, el .cpg, la nombra; sin él, cada lector adivina, y dos lectores adivinan distinto. Adivinar mal de una manera convierte Amapá en Amapá; adivinar mal de la otra lo convierte en Amap�. Los dos se ven distintos, así que la basura te dice de qué lado te equivocaste, y la solución es un .cpg de una línea o un comando ogr2ogr que declare la entrada antes de recodificarla.
Todo lo de abajo fue probado el 16 de septiembre de 2026 con GDAL 3.13.3 y el shapefile-viewer en su build de septiembre de 2026 (static-page-tools@442f8d8 más el cambio del .cpg suelto descrito abajo, sin publicar cuando esto se escribió y desplegado antes de este post); lo que imprimió cada herramienta está citado tal cual. El dataset es la malla 2022 del IBGE de los 27 estados brasileños, cuyo .dbf es UTF-8 y cuyo .cpg lo dice: Amapá se guarda como los dos bytes c3 a1. La guía de Shapefile explica el mecanismo bajo "Latin-1 por default, y el archivo lateral .cpg"; este post es la reproducción y las soluciones.
Dos direcciones, dos tipos de basura distintos
Bytes UTF-8 leídos como Latin-1: borrá el .cpg del archivo del IBGE y decile a GDAL que es Latin-1 con ogrinfo -al BR_UF_2022.shp -fields=YES -geom=NO --config SHAPE_ENCODING ISO-8859-1, y NM_UF imprime Amapá y São Paulo. Cada secuencia UTF-8 de dos bytes se leyó como dos letras Latin-1, así que el texto se alargó y no se perdió nada; exportado a GeoJSON de la misma manera, el archivo guarda "NM_UF":"Amapá" para siempre. Ese es el daño estilo é que vio cualquier tabla de atributos latinoamericana: un lector con default Latin-1 encontrándose con un archivo UTF-8.
Bytes Latin-1 leídos como UTF-8: un .dbf Latin-1 guarda á como el byte único e1, que no es UTF-8 válido, así que un decodificador UTF-8 lo reemplaza por U+FFFD, el glifo �, y la letra desaparece. Esta es la dirección del navegador. El caso real es el Shapefile de tierras indígenas de la FUNAI: byte 29 del encabezado en 00, sin .cpg, y 288 de sus 665 nombres se imprimen como Acim� por default y como Acimã solo cuando a GDAL se le indica ISO-8859-1. Una precisión sobre las transcripciones de GDAL: sin nada en qué basarse, GDAL pasa el byte e1 sin tocar, y el � que ves es tu terminal mostrando un byte inválido; solo el decodificador del visor del navegador escribe de verdad U+FFFD en el valor.
Cómo se genera un archivo Latin-1 sin .cpg
No hace falta nada exótico. Un ogr2ogr -f "ESRI Shapefile" uf.shp BR_UF_2022.shp simple, sin ninguna opción de codificación, alcanza: GDAL 3.13.3 escribe un .dbf Latin-1, pone el byte del driver de idioma en el offset 29 en 87, y no escribe ningún .cpg, aunque el .cpg de origen dijera UTF-8; el c3 a1 de origen se convierte en e1 en el camino. GDAL lee ese archivo de vuelta correctamente porque respeta el byte 29 (ogrinfo reporta ENCODING_FROM_LDID=ISO-8859-1), así que nada se ve mal hasta que el archivo llega a un lector que nunca mira el byte 29, como el visor del navegador. La forma más rara es la de la FUNAI, byte 29 en 00 y sin .cpg, donde ni GDAL adivina nada (SOURCE_ENCODING= vuelve vacío) y pasa los bytes sin tocar.
Qué muestra el visor del navegador
El visor lee el .dbf con un solo decodificador, UTF-8 por default, y cambia a lo que diga el .cpg. La misma malla de 27 estados, convertida a Latin-1 con -lco ENCODING=ISO-8859-1 (byte 29 en 00, un .cpg de diez bytes que dice ISO-8859-1), se soltó de cuatro maneras. Cada prueba leyó 27 polígonos y 5 campos en el panel de capas, y escribir amap en el cuadro Buscar en 27 entidades de la tabla de atributos devolvió una fila, 4 en Número de fila y Multipolígono en Tipo; la celda NM_UF es lo que cambia:
- Zip con el
.cpgadentro:AmapáySão Paulo, decodificados como Latin-1 porque el.cpglo decía. - El mismo zip con el
.cpgborrado:Amap�yS�o Paulo. .shp,.dbf,.prjy.cpgsueltos, soltados juntos:Amapá. Esto es nuevo: desde su cambio de septiembre de 2026 el visor respeta un.cpgsoltado junto a los archivos sueltos; antes de eso, los archivos sueltos siempre se leían como UTF-8, dijera lo que dijera el sidecar.- Archivos sueltos sin el
.cpg:Amap�.
Dos pruebas más cierran el panorama. El archivo original UTF-8 con su .cpg borrado sigue leyendo Amapá en el navegador, porque el default UTF-8 es correcto para bytes UTF-8; es el mismo archivo que se convierte en Amapá bajo el SHAPE_ENCODING ISO-8859-1 de GDAL. Y un archivo con la misma forma que la salida por default de GDAL, byte 29 en 87 y sin .cpg, lee Amap� en el visor tanto zipeado como suelto: el parser DBF del visor nunca lee el byte 29 (grep -c 'getUint8(29)' node_modules/parsedbf/index.js imprime 0). GDAL lee ese byte, el visor no, y el .cpg es la única señal en la que ambos están de acuerdo.
Solución 1: escribir el .cpg
Si los bytes son Latin-1, decilo. Una línea, sin necesidad de salto de línea: printf 'ISO-8859-1' > BR_UF_2022_l1.cpg, al lado del .dbf con el mismo nombre base. Los dos lectores lo respetaron: ogrinfo reporta ENCODING_FROM_CPG=ISO-8859-1 y imprime Amapá, y el visor, con el zip que tiene el archivo nuevo adentro, muestra Amapá de nuevo. Escribilo ISO-8859-1. GDAL también acepta LATIN1, latin1 y el numérico de ESRI 88591, y perdona un salto de línea final, pero el navegador le pasa la etiqueta al TextDecoder estándar, que no conoce 88591 y cae de vuelta a UTF-8: un .cpg que dice 88591 lee Amap� en el visor mientras lee Amapá en GDAL.
Solución 2: recodificar a UTF-8, y declarar la entrada
Si preferís entregar un archivo UTF-8, el comando seguro nombra tanto la codificación de entrada como la de salida: ogr2ogr -f "ESRI Shapefile" fixed.shp BR_UF_2022_l1.shp -oo ENCODING=ISO-8859-1 -lco ENCODING=UTF-8. Después, cat fixed.cpg imprime UTF-8, los bytes guardados para á son c3 a1, ogrinfo reporta ENCODING_FROM_CPG=UTF-8 e imprime Amapá, y el visor muestra Amapá desde el zip. El .cpg y los bytes concuerdan, y todos los lectores dan la misma respuesta.
La trampa: -lco ENCODING=UTF-8 solo
El atajo tentador es -lco ENCODING=UTF-8 sin el -oo, y que funcione depende de si GDAL sabía qué estaba leyendo. En un archivo cuya codificación GDAL no puede determinar (sin .cpg, byte 29 en 00, la forma de la FUNAI) copia los bytes Latin-1 sin tocar y etiqueta la copia: cat out.cpg imprime UTF-8, el .dbf sigue con e1, y ogrinfo reporta ENCODING_FROM_CPG=UTF-8 mientras imprime Amap�. El .cpg miente, y le miente a todos: el visor muestra Amap� para esa salida y GDAL muestra lo mismo. Un archivo Latin-1 que ya trae un .cpg equivocado que dice UTF-8 sigue el mismo camino, una copia sin efecto que arrastra la mentira. En la salida por default de GDAL mismo, byte 29 en 87, el atajo anda bien: GDAL confía en el byte del encabezado, lee Latin-1 y escribe un c3 a1 genuino con un .cpg verdadero. Como rara vez sabés cuál de las tres formas te tocó, -oo ENCODING=ISO-8859-1 no cuesta nada y es correcto en las tres. En QGIS el equivalente es el menú desplegable de codificación del origen de datos en las propiedades de la capa; cambia cómo QGIS lee el archivo, no el archivo.
Archivos de prueba que podés usar
El dataset es la malla estatal 2022 del IBGE, 13.717.460 bytes, SHA-256 282ec7f0f0beeeead45e6609f4ffffce161bda04cb8ee0afcad2316d1c841bcb; el listado del directorio no imprime ninguna línea de licencia, así que dale crédito al IBGE y revisá los términos del portal antes de republicar.
Para armar la variante rota, convertí a Latin-1 con ogr2ogr -f "ESRI Shapefile" latin1/BR_UF_2022_l1.shp BR_UF_2022.shp -lco ENCODING=ISO-8859-1, y después borrá latin1/BR_UF_2022_l1.cpg. Zipeá los cuatro archivos restantes, o soltalos sueltos, en el shapefile-viewer y buscá amap. El post de comparación midió la otra dirección sobre el mismo archivo bajo "Encoding", junto a GeoPackage y GeoJSON, que son UTF-8 por especificación y no tienen ningún sidecar que perder.
Para equipos
Una tilde que sobrevive en una máquina y muere en la siguiente es un archivo de cinco bytes que nadie sabía que había que mandar. Geodocs guarda los valores de tus campos como UTF-8 en un mapa compartido, así que Amapá se lee igual en cada dispositivo y la pregunta de la codificación aparece recién en la exportación, cuando ya sabés qué cinco bytes escribir.