Wrong accents in a Shapefile: .cpg, Latin-1 and UTF-8
Short answer: the .dbf that holds a Shapefile's attributes carries no encoding of its own. A sidecar of a few bytes, the .cpg, names one; without it every reader guesses, and two readers guess differently. Guess wrong one way and Amapá becomes Amapá; guess wrong the other way and it becomes Amap�. The two look different, so the garbage tells you which way you were wrong, and the fix is a one-line .cpg or one ogr2ogr command that declares the input before recoding it.
Everything below was tested on 16 September 2026 with GDAL 3.13.3 and the shapefile-viewer in its September 2026 build (static-page-tools@442f8d8 plus the loose-.cpg change described below, unreleased when this was written and shipped before this post); what each tool printed is quoted. The dataset is IBGE's 2022 mesh of the 27 Brazilian states, whose .dbf is UTF-8 and whose .cpg says so: Amapá is stored as the two bytes c3 a1. The Shapefile guide explains the mechanism under "Latin-1 by default, and the .cpg sidecar"; this post is the reproduction and the fixes.
Two directions, two different kinds of garbage
UTF-8 bytes read as Latin-1: delete the IBGE file's .cpg and tell GDAL it is Latin-1 with ogrinfo -al BR_UF_2022.shp -fields=YES -geom=NO --config SHAPE_ENCODING ISO-8859-1, and NM_UF prints Amapá and São Paulo. Each two-byte UTF-8 sequence was read as two Latin-1 letters, so the text got longer and nothing was lost; exported to GeoJSON the same way, the file carries "NM_UF":"Amapá" for good. That is the é-style damage every Latin American attribute table has seen: a reader with a Latin-1 default meeting a UTF-8 file.
Latin-1 bytes read as UTF-8: a Latin-1 .dbf stores á as the single byte e1, which is not valid UTF-8, so a UTF-8 decoder replaces it with U+FFFD, the � glyph, and the letter is gone. This is the browser's direction. The real-world case is FUNAI's indigenous-lands Shapefile: header byte 29 at 00, no .cpg, and 288 of its 665 names print as Acim� by default and as Acimã only when GDAL is told ISO-8859-1. One precision about GDAL transcripts: with nothing to go on, GDAL passes the byte e1 through unchanged, and the � you see is your terminal rendering an invalid byte; only the browser viewer's decoder actually writes U+FFFD into the value.
How a Latin-1 file without a .cpg gets made
Not by anything exotic. A plain ogr2ogr -f "ESRI Shapefile" uf.shp BR_UF_2022.shp, with no encoding option, is enough: GDAL 3.13.3 writes a Latin-1 .dbf, sets the language-driver byte at offset 29 to 87, and writes no .cpg at all, even when the source's own .cpg said UTF-8; the source's c3 a1 becomes e1 on the way through. GDAL reads that file back correctly because it honours byte 29 (ogrinfo reports ENCODING_FROM_LDID=ISO-8859-1), so nothing looks wrong until the file reaches a reader that never looks at byte 29, such as the browser viewer. The rarer shape is the FUNAI one, byte 29 at 00 and no .cpg, where even GDAL guesses nothing (SOURCE_ENCODING= comes back empty) and passes the bytes through.
What the browser viewer shows
The viewer reads the .dbf with one decoder, UTF-8 by default, and switches to whatever the .cpg names. The same 27-state mesh, converted to Latin-1 with -lco ENCODING=ISO-8859-1 (byte 29 at 00, a ten-byte .cpg saying ISO-8859-1), was dropped four ways. Every drop read 27 polygons and 5 fields in the layers panel, and typing amap into the attribute table's Search 27 features box returned one row, 4 under Row number and MultiPolygon under Type; the NM_UF cell is what differs:
- Zip with the
.cpginside:AmapáandSão Paulo, decoded as Latin-1 because the.cpgsaid so. - The same zip with the
.cpgdeleted:Amap�andS�o Paulo. - Loose
.shp,.dbf,.prjand.cpgdropped together:Amapá. This one is new: since its September 2026 change the viewer honours a.cpgdropped beside the loose files; before that, loose files were always read as UTF-8 whatever the sidecar said. - Loose files without the
.cpg:Amap�.
Two more drops close the picture. The original UTF-8 file with its .cpg deleted still reads Amapá in the browser, because the UTF-8 default is right for UTF-8 bytes; it is the same file that turns into Amapá under GDAL's SHAPE_ENCODING ISO-8859-1. And a file of the same shape as GDAL's own default output, byte 29 at 87 and no .cpg, reads Amap� in the viewer both zipped and loose: the viewer's DBF parser never reads byte 29 (grep -c 'getUint8(29)' node_modules/parsedbf/index.js prints 0). GDAL reads that byte, the viewer does not, and the .cpg is the one signal both agree on.
Fix 1: write the .cpg
If the bytes are Latin-1, say so. One line, no newline needed: printf 'ISO-8859-1' > BR_UF_2022_l1.cpg, next to the .dbf with the same basename. Both readers honoured it: ogrinfo reports ENCODING_FROM_CPG=ISO-8859-1 and prints Amapá, and the viewer, given the zip with the new file inside, shows Amapá again. Spell it ISO-8859-1. GDAL also accepts LATIN1, latin1 and ESRI's numeric 88591, and forgives a trailing newline, but the browser hands the label to the standard TextDecoder, which does not know 88591 and falls back to UTF-8: a .cpg saying 88591 reads Amap� in the viewer while reading Amapá in GDAL.
Fix 2: recode to UTF-8, and declare the input
If you would rather ship a UTF-8 file, the safe command names both the input and the output encoding: ogr2ogr -f "ESRI Shapefile" fixed.shp BR_UF_2022_l1.shp -oo ENCODING=ISO-8859-1 -lco ENCODING=UTF-8. Afterwards cat fixed.cpg prints UTF-8, the stored bytes for á are c3 a1, ogrinfo reports ENCODING_FROM_CPG=UTF-8 and prints Amapá, and the viewer shows Amapá from the zip. The .cpg and the bytes agree, and every reader gets the same answer.
The trap: -lco ENCODING=UTF-8 alone
The tempting shortcut is -lco ENCODING=UTF-8 without the -oo, and whether it works depends on whether GDAL knew what it was reading. On a file whose encoding GDAL cannot determine (no .cpg, byte 29 at 00, the FUNAI shape) it copies the Latin-1 bytes unchanged and labels the copy: cat out.cpg prints UTF-8, the .dbf still holds e1, and ogrinfo reports ENCODING_FROM_CPG=UTF-8 while printing Amap�. The .cpg lies, and it lies to everyone: the viewer shows Amap� for that output and GDAL shows the same. A Latin-1 file that already carries a wrong .cpg saying UTF-8 goes the same way, a no-op copy with the lie carried forward. On GDAL's own default output, byte 29 at 87, the shortcut is fine: GDAL trusts the header byte, reads Latin-1 and writes genuine c3 a1 with a truthful .cpg. Since you rarely know which of the three shapes you were handed, -oo ENCODING=ISO-8859-1 costs nothing and is right in all of them. In QGIS the equivalent is the data source encoding drop-down in the layer's properties; it changes how QGIS reads the file, not the file.
Test files you can use
The dataset is IBGE's 2022 state mesh, 13,717,460 bytes, SHA-256 282ec7f0f0beeeead45e6609f4ffffce161bda04cb8ee0afcad2316d1c841bcb; the directory listing prints no licence line, so credit IBGE and check the portal's terms before republishing.
To make the broken variant, convert to Latin-1 with ogr2ogr -f "ESRI Shapefile" latin1/BR_UF_2022_l1.shp BR_UF_2022.shp -lco ENCODING=ISO-8859-1, then delete latin1/BR_UF_2022_l1.cpg. Zip the four remaining files, or drop them loose, into the shapefile-viewer and search for amap. The comparison post measured the other direction on the same file under "Encoding", beside GeoPackage and GeoJSON, which are UTF-8 by specification with no sidecar to lose.
For teams
An accent that survives on one machine and dies on the next is a five-byte file nobody knew to ship. Geodocs keeps your field values as UTF-8 on one shared map, so Amapá reads the same on every device and the encoding question only comes up at export, when you now know which five bytes to write.