# 06 — Qualidade textual das denominações (`n`) e campos de texto

- **Painel:** MonumentosES — auditoria de fidelidade, agente 6/8
- **Data:** 2026-09-04
- **Dataset auditado:** `www/explorer/bic_data.json` (**22.237 records**, 15 fontes — estado de 2026-09-04 15:15 [C]; nota: este ficheiro foi reconstruído depois do relatório 05 ter corrido, daí as contagens diferentes no 05, cf. Desvios)
- **Marcações:** [C] = confirmado programaticamente no estado atual do repositório; [I] = inferido/secundário (raciocínio ou documento não executado nesta auditoria)

---

## Âmbito

1. **Mojibake** — artefactos de encoding nas 22.237 denominações da vista (`n`): padrões clássicos UTF-8-lido-como-latin1/cp1252 (`Ã`, `â€`, `Ì`, U+FFFD) e mojibake DOS (CP850/CP437 lido como latin-1: `¤`, `¢`, `£`, NBSP, `¡`), com contagem e exemplos por fonte; risco particular nas fontes cp1252/latin1 (mad, clm, nav DBF) e no ODS galego.
2. **Maiúsculas inconsistentes** — % de denominações integralmente em maiúsculas (all-caps) por fonte; recomendação de tratamento.
3. **Truncagens** — comprimento máximo de `n` por fonte; sinais de corte (concentração num teto, parêntesis por fechar, cortes a meio de sintagma); confirmação nas fontes cruas.
4. **Denominações vazias / placeholder `(sin denominación)`** — contagem e forma de apresentação na vista.
5. **Espaços duplicados e caracteres de controlo** — varredura integral de `n` (e nota aos restantes campos de texto `s`, `t2`, `ou`, `cu`, `pr`, `owt`).

## Método

- Python 3.12, leitura apenas — nenhum ficheiro de dados foi reescrito [C].
- **Mojibake:** (a) regex de artefactos clássicos (`Ã`, `â€`, `Ì`, `Â`, U+FFFD, …); (b) teste de dupla codificação — `n.encode('latin-1'/'cp1252').decode('utf-8')` com sucesso e com letras = suspeito; (c) mapa completo de bytes latin-1↔CP850 que divergem, aplicado como `str.translate` a todas as `n`, com filtragem manual dos falsos positivos (interpunct catalão `·`, separador `·` do campo `pr`); (d) varredura de caracteres C1 (U+0080–009F) residuais.
- **Fontes de risco verificadas nos ingests:** `tools/ingest_madrid.py:29` lê `raw/mad_inmuebles_bic.csv` com `encoding="cp1252"` [C]; `tools/ingest_castilla-la-mancha.py:41` lê `raw/clm_catalogo.csv` com `cp1252` [C]; `tools/ingest_navarra.py:42` lê os DBF via pyshp com `encoding="cp1252"` [C]. Spot-check do mad cru (é latin-1: `Jes�s` em bytes) → na vista chega `Jesús` correto [C].
- **Truncagens:** histograma de `len(n)` por fonte; comparação do nome mais longo de mad/gva/eus com a fonte crua (`raw/mad_inmuebles_bic.csv`, `raw/gva_bic_inmuebles.csv`, `raw/eus_L2_1.json`/`eus_L2_7.json`).
- **Vista:** inspeção de `tools/build_site_view.py` (gera `bic_data.json`), `www/js/explorer.js` (renderização de cartões, pesquisa, ordenação), `www/js/monument.js` (ficha), `site/public/i18n/*.js` e `www/i18n/*.js`.

## Achados

### 1. Mojibake — 0 do tipo clássico; 28 registos `arg` com mojibake CP850 já na fonte crua

- **Padrões clássicos UTF-8↔latin1/cp1252 (`Ã`, `â€`, `Ì`, `Â`, U+FFFD, C1 residuais): 0 ocorrências em 22.237 `n`** [C].
- **Dupla codificação re-decodificável (latin1/cp1252→utf-8): 0 registos** [C].
- **Mojibake CP850/CP437→latin-1: 28 registos, todos da fonte `arg`** [C]. Os caracteres `¤`(0xA4→`ñ`), `¢`(0xA2→`ó`), `\xa0`(0xA0→`á`), `¡`(0xA1→`í`) são exatamente as letras espanholas do CP850 lidas como latin-1. **O mojibake já está gravado na fonte crua** — `raw/arg_bic_puntos.csv` (hoje UTF-8) contém `Ca¤ada` e `Balc¢n` [C]; o ingest (`tools/ingest_aragon.py:34`, `encoding="utf-8"`) propaga-o fielmente [C]. Exemplos (fix = re-codificação CP850) [C]:

| id | na vista | correção CP850 |
|---|---|---|
| arg:1632 | `Ca¤ada del Campillo I` | Cañada del Campillo I |
| arg:1583 | `Abrigo de las Pe¤as` | Abrigo de las Peñas |
| arg:1656 | `El Balc¢n de Forniellos II` | El Balcón de Forniellos II |
| arg:1669 | `Front¢n I de Forniellos` | Frontón I de Forniellos |
| arg:1667 | `Ermita de Santa B\xa0rbara` (NBSP) | Ermita de Santa Bárbara |
| arg:1587 | `Abrigo de San Mart¡n` | Abrigo de San Martín |
| arg:1733 | `Roca Bened¡` | Roca Benedí |

  Distribuição dos 28: `¤` 11, `¢` 8, `¡` 6, NBSP 4 (alguns registos têm mais do que um) [C]. **Impacto real:** a pesquisa do explorador (substring direta, `explorer.js:301–309`) não encontra estes registos pela grafia correta — procurar «Cañada», «Peñas» ou «Balcón» dá 0 resultados para eles [C]; a ordenação e as citações BibTeX da ficha (`monument.js:157,163`) exportam o carácter corrompido [C].
- **Falsos positivos excluídos:** 5 registos `cat` com `·` são o interpunct catalão legítimo (`COL·LEGIATA`, `VIL·LA`) [C]; as 2.219 ocorrências de `·` no campo `pr` são o separador de data do próprio pipeline (`2002-03-08 · BOA 27/03/2002`) [C]. O en-dash `–` em 7 registos gal (`Petróglifo Taleiriña – Aragunde`) é tipografia legítima do ODS galego [C].
- **Fontes de risco sem qualquer mojibake:** mad (0), clm (0), nav (0), gal ODS (0) — as leituras cp1252/latin-1 nos ingests estão corretas [C]. O `¤`/`¢` é o único defeito de encoding de todo o dataset [C].

### 2. Maiúsculas — 3 fontes praticamente 100% all-caps, 2 mistas, resto em caixa normal

Denominações integralmente em maiúsculas (≥3 letras, todas upper) por fonte [C]:

| fonte | n | all-caps | % |
|---|---|---|---|
| cyl | 2.479 | 2.470 | **99,6%** |
| cat | 2.161 | 2.148 | **99,4%** |
| ten | 199 | 196 | **98,5%** |
| ast | 331 | 233 | 70,4% |
| eus | 10.295 | 5.565 | 54,1% |
| arg | 1.021 | 540 | 52,9% |
| eiv | 728 | 4 | 0,5% |
| gva | 1.185 | 1 | 0,1% |
| cant/clm/ext/gal/mad/nav/rio | 3.655 | 0 | 0,0% |

- No cartão do explorador a lista mistura visualmente `HÓRREO POSADA_04` (cyl) com `Torre fuerte de los García Marrón en Cellarorigo`-estilo (rio/mad/gal) [C].
- Não existe nenhum `text-transform` no CSS do site (`site/public/monumentos.css`: 0 ocorrências) — a caixa mostrada é exatamente a do dado [C].
- **Recomendação: não tocar no dado** (é o texto oficial das fontes); tratar na camada de apresentação, se desejado (cf. Recomendações).

### 3. Truncagens — 1 corte confirmado (eus, já na fonte); resto sem sinais

- **eus: 1 denominação cortada a exatamente 120 caracteres** com parêntesis por fechar: `Infraestructura hidráulica del Molino de Villanueva de Valdegovia (Anterior Presa del Molino de Villanueva de Valdegovía` — **confirmado na fonte crua** (`raw/eus_L2_1.json` e `eus_L2_7.json`, campo `NOMBRE_ES`, idêntico) [C]. É portanto truncagem de coluna da fonte (Eusko Jaurlaritza), não do pipeline [C]. No total, 4 registos eus têm parêntesis não fechado (`n(` ≠ `n)`), ex.: `STA 4 (ANTIGUA CASA PLAZA DE D. MIGUEL DÍAZ DE TUESTA 8` [C].
- **mad: sem truncagem.** O nome mais longo (301 car., `Templo Nacional de Santa Teresa de Jesús…`) está completo e idêntico na fonte crua (campo `denominacion_registral`, latin-1 corretamente decodificado) [C]. A aparente distribuição longa (mediana 61 car.) deve-se ao registo de Madrid incluir endereços na denominação — é assim na fonte [C].
- **Sem concentrações suspeitas:** o histograma de `len(n)` é suave em todas as fontes (sem pico num teto único; máximos: eus 120, ten 117, gva 116, cyl 108, ext 111, mad 301) [C].
- gva máx. 116 (`Escudo de los Marco Lloris…o de los Marco`) termina estranho mas o campo completo na fonte crua é igual — não há corte do pipeline [C].

### 4. `(sin denominación)` — 99 registos (gal 97 + rio 2), placeholder injetado no build e nunca traduzido

- **99 registos com `n` vazio na origem: gal 97 + rio 2** (confirma a estimativa ~97+2) [C]. Todos os 99 têm também o subtipo `t2` a `-` ou texto e **subtítulo `s` vazio** [C]. Ex.: `gal:BIC.000.122` (Monforte de Lemos), `gal:BIC.000.130` (Arte rupestre, Arbo) [C].
- O placeholder **não vem da fonte — é injetado no build**: `tools/build_site_view.py:58` → `"n": it.get("n") or "(sin denominación)"` [C]. O `bic_data.json` final contém 99 ocorrências da string [C].
- **Apresentação:** `explorer.js:401` — `${r.n ? esc(r.n) : t('card.noname')}`. Como após o build `n` nunca fica vazio, o ramo `t('card.noname')` é **código morto**: as traduções pt `(sem nome)`, en `No name`, fr `Sans nom`, de `Ohne Namen` (`site/public/i18n/pt.js:14` etc.) nunca são mostradas; o utilizador das versões pt/en/fr/de vê sempre o placeholder espanhol `(sin denominación)` [C]. Não existe `i18n/es.js` (o espanhol é a língua de base) [C].
- Na ficha (`monument.js`), o placeholder entra no `<h1>`, no `document.title`, na citação BibTeX/RIS e no link para o Google Maps [C].
- Na ordenação por nome (`explorer.js:317`, `localeCompare(…, 'fr')`) os 99 ficam agrupados no topo sob `(` [C].

### 5. Espaços duplicados e caracteres de controlo

- **Espaço duplo em `n`: 169 registos** [C]: eus 103 (`IGLESIA  DE SAN MARTIN`), ten 21 (`EL  PUERTO DE LA CRUZ`), cyl 8, mad 8, nav 7, cat 5, gva 5, ext 4, arg 4 (`Abrigo del Arquero de los Callejones  Cerrados`), eiv 2, ast 1, rio 1. Em HTML o browser colapsa os espaços, pelo que o efeito visível é mínimo; afeta a pesquisa por frase exata e as citações [C; I impacto visual].
- **Quebras de linha dentro do nome: 4 registos** [C]: arg:955 e arg:298 (`CASTILLO DE LOS FERNANDEZ\n\nDE HEREDIA…`), cat:618-539-MH (`TORRE DE LA SERRA DE LA PETITA\r\nCastell dels moros - Fort de Luisa Fernanda\r\n - 539-MH` — múltiplas denominações concatenadas com CR+LF da fonte), eus:190-01028 (`Casa Costanilla del Hospital 12\n\n\n 12`). São 9×U+000A + 2×U+000D no total [C].
- **Sem** whitespace nas pontas (0), tabs (0), zero-width (0), U+FFFD (0) [C]. NBSP existe apenas nos 4 registos arg do mojibake CP850 [C].

### 6. Outros campos de texto (nota breve)

- `s` (subtítulo): **vazio em 22.237/22.237** — campo sem uso nesta versão [C].
- `ou`/`cu`: ~200 registos, valores controlados curtos (`Religioso`) sem defeitos [C]; `owt`: 100% vazio [C].
- **Apóstrofo escrito como acento agudo `´`: 6 registos** (ast 2, cat 1, gva 3): `L´HOMÓN DE FARO`, `D´ÀNEU`, `Castillo y Murallas de Callosa D´En Sarrià`, `d´en Garcés`, `Torre de L´Oró` [C].
- **Underscores no nome: 418 registos** [C]: cyl 382 com numeração da fonte para desambiguar duplicados (`HÓRREO POSADA_01`…`_09`, `CUEVA DEL SILO_00`) e arg 36 com sufixo descritivo (`Masada de Ligros II_Grabado 6`, `Castillo de los Ares_Arte Rupestre`) [C].
- **Aspas inconsistentes:** 72 registos com aspas retas `"` (cyl 36, eus 14, cat 9, ext 7, eiv 3, cant 2, ast 1) vs. 15 com curvas `“”’` (maioria eiv: `d’en Cires`) [C].

## Desvios

1. **[C] Dataset mudou no meio do painel:** `www/explorer/bic_data.json` foi reconstruído às 15:15 de 2026-09-04; o relatório 05 (15:01) auditou 24.337 records e este 06 audita **22.237**. Todas as contagens aqui são do estado 15:15. Recomenda-se re-corrida dos relatórios anteriores sobre o estado final (ou fixar a versão auditada).
2. **[C] Fora do âmbito estrito, mas com impacto direto na apresentação das denominações:** `www/js/monument.js` (carregado por `www/monumento/index.html:17`, `?v=1`) é uma adaptação do template Mérimée/SIPA francês e mantém **7–8 referências a "Mérimée"/"France"/data.gouv"**: badge `Mérimée {id}` junto ao `<h1>` da denominação (`monument.js:273`), citação `mon.n (Mérimée …). Base Mérimée, Ministère de la Culture (France)` (`:152`), BibTeX/RIS com Mérimée (`:157,163`) e link Google Maps com `, France` (`:269`). A mesma filiação francesa aparece no rodapé da ficha. → Confirmação e correção pertencem ao agente de identidade/fontes (01), mas como o `<h1>` com a denominação é o contexto aqui auditado, fica registado.
3. **[C] `localeCompare(…, 'fr')`** na ordenação de nomes (`explorer.js:317`) — para texto espanhol/galego esperava-se `'es'` (afeta subtilmente a ordem de `ñ`, acentos e do placeholder `(…`).

## Recomendações

1. **Corrigir os 28 mojibake `arg`** (prioridade máxima; 0,13% do dataset, 100% corrigível deterministicamente): recodificar na fonte `raw/arg_bic_puntos.csv` com `col.encode('latin-1').decode('cp850')` nos campos de texto, ou patch pontual no `ingest_aragon.py` com o mapa `¤→ñ, ¢→ó, ¡→í, \xa0→á, £→ú`. Ganho: pesquisa por grafia correta passa a encontrar estes BIC; citações deixam de exportar `¤`. *(Opcionalmente reportar ao NAHRIS/Aragón Open Data.)*
2. **i18n do placeholder:** em vez de injetar `(sin denominación)` no build (`build_site_view.py:58`), manter `n` vazio no JSON e resolver no render (`r.n || t('card.noname')`) — assim pt/en/fr/de mostram `(sem nome)`/`No name`/… e o espanhol mantém o atual. Alternativa mínima: injetar por língua no build multilingue. *(Não afeta os 99 registos em si, só a apresentação.)*
3. **Normalização leve no build (sem tocar na fonte):** colapsar `\s+`→um espaço e fazer `strip()` em `n` no `build_site_view.py` — elimina os 169 espaços duplos, as 4 quebras de linha e blinda contra futuros casos; para o cat:618-539-MH, avaliar trocar o separador `\r\n` por ` · ` ou escolher a primeira denominação.
4. **Maiúsculas: não editar o dado** (é o texto oficial das fontes). Se se quiser harmonizar visualmente nos cartões: (a) baixo risco — CSS `font-variant: small-caps` ou nada; (b) médio risco — title-case só em apresentação, com lista de stopwords espanholas (`de, del, la, y, en, …`) e preservação de siglas; nunca persistir. Dado o mix (54% caps / 46% misto no eus), qualquer transformação parcial criaria nova inconsistência — recomenda-se deixar como está ou aplicar a todas as fontes por igual.
5. **Truncagem eus (1 registo):** documentar como limitação da fonte na metodologia; opcionalmente reportar à Eusko Jaurlaritza (open data). Os restantes 3 parêntesis não fechados são ruído da fonte, idem.
6. **Apóstrofo `´`→`'`**: correção pontual em 6 registos (display ou build; a fonte pode manter-se).
7. **Ordenação:** trocar `localeCompare(…, 'fr')` por `'es'` em `explorer.js` (e afins).

## Fontes

Estado auditado (repositório local `C:\Users\Atelier X\Nextcloud\Documents\00_Website\MonumentosES`, 2026-09-04 15:15+):

- `www/explorer/bic_data.json` — vista publicada, 22.237 records (campo `n`, `s`, `t2`, `ou`, `cu`, `pr`, `src`) [C]
- `www/js/explorer.js` — cartões (`:401`), pesquisa (`:301–309`), ordenação (`:317`) [C]
- `www/js/monument.js` — ficha/denominação, vestígios Mérimée (`:152,157,163,228,269,273`) [C]
- `www/monumento/index.html` — carregamento de `/js/monument.js?v=1` [C]
- `tools/build_site_view.py:58` — injeção do placeholder `(sin denominación)` [C]
- `tools/build_patterns_es.py:113` — uso do mesmo placeholder para `t2` [C]
- `tools/ingest_aragon.py:34` — leitura UTF-8 do CSV arg [C]
- `tools/ingest_madrid.py:29`, `tools/ingest_castilla-la-mancha.py:41`, `tools/ingest_navarra.py:42` — leituras cp1252 (mad/clm CSV, nav DBF/pyshp) [C]
- `raw/arg_bic_puntos.csv` — mojibake `Ca¤ada`/`Balc¢n` já presente [C]
- `raw/mad_inmuebles_bic.csv` — denominação registral de 301 car. completa, latin-1 [C]
- `raw/eus_L2_1.json`, `raw/eus_L2_7.json` — `NOMBRE_ES` truncado a 120 car. na fonte [C]
- `raw/gva_bic_inmuebles.csv` — campo denominação completo (idêntico à vista) [C]
- `raw/gal_bic.ods` — en-dashes legítimos; sem mojibake [C]
- `site/public/monumentos.css` — 0 ocorrências de `text-transform` [C]
- `site/public/i18n/{pt,en,fr,de}.js`, `www/i18n/*` — `card.noname` (não existe `es.js`) [C]
- Relatórios irmãos: `site/public/audit/05-coordenadas.md` (contagens de um estado anterior do dataset) [C]

---

## Comandos

```bash
cd "C:\Users\Atelier X\Nextcloud\Documents\00_Website\MonumentosES"

# 1. mojibake clássico + dupla codificação + CP850 (contagens do relatório)
python - <<'EOF'
import json, re
from collections import Counter
recs = json.load(open('www/explorer/bic_data.json', encoding='utf-8'))['records']
print('total:', len(recs))
# (a) clássicos
pats = [r'Ã', r'â€', r'Ì', r'Â', r'\ufffd', r'Å']
print('clássicos:', sum(1 for r in recs if any(re.search(p, r.get('n') or '') for p in pats)))
# (b) dupla codificação utf-8<-latin1/cp1252
dbl = 0
for r in recs:
    n = r.get('n') or ''
    for enc in ('cp1252','latin-1'):
        try: t = n.encode(enc).decode('utf-8')
        except Exception: continue
        if t != n and any(c.isalpha() for c in t): dbl += 1; break
print('dupla codificação:', dbl)
# (c) CP850->latin1 (símbolos que em cp850 são letras ES)
m = {'¤':'ñ','¢':'ó','£':'ú','\xa0':'á','¡':'í','µ':'Á','¶':'Â','·':'À','¦':'ª'}
hit = [r for r in recs if any(c in m for c in (r.get('n') or ''))]
print('CP850 em n:', Counter(r['src'] for r in hit))
for r in hit:
    if r['src']=='arg': print(' ', r['i'], repr(r['n']), '=>', r['n'].translate(str.maketrans(m)))
EOF

# 2. % all-caps por fonte
python - <<'EOF'
import json
from collections import defaultdict
recs = json.load(open('www/explorer/bic_data.json', encoding='utf-8'))['records']
def caps(n):
    L=[c for c in n if c.isalpha()]; return len(L)>=3 and all(c.isupper() for c in L)
st = defaultdict(lambda:[0,0])
for r in recs:
    n=(r.get('n') or '').strip()
    if n: st[r['src']][1]+=1; st[r['src']][0]+= caps(n)
for s,(a,b) in sorted(st.items(), key=lambda kv:-kv[1][0]/max(kv[1][1],1)):
    print(f'{s}: {a}/{b} = {100*a/b:.1f}%')
EOF

# 3. truncagens: len máx, parêntesis não fechados, concentrações
python - <<'EOF'
import json
from collections import Counter
recs = json.load(open('www/explorer/bic_data.json', encoding='utf-8'))['records']
for src in sorted({r['src'] for r in recs}):
    L = sorted(len(r.get('n') or '') for r in recs if r['src']==src)
    print(src, 'max=', L[-1], 'p99=', L[int(len(L)*0.99)])
print('parêntesis não fechado:',
      Counter(r['src'] for r in recs if (r.get('n') or '').count('(')!=(r.get('n') or '').count(')')))
EOF
# confirmação na fonte crua (eus 120; mad 301 completo):
grep -o 'Infraestructura hidráulica[^\"]*' raw/eus_L2_1.json | head -1
python -c "
import csv; csv.field_size_limit(10**8)
r=[row['denominacion_registral'] for row in csv.DictReader(open('raw/mad_inmuebles_bic.csv',encoding='cp1252'),delimiter=';')]
mx=max(r,key=len); print(len(mx), repr(mx[-60:]))"

# 4. '(sin denominación)' e placeholder no build
python -c "
import json
recs = json.load(open('www/explorer/bic_data.json', encoding='utf-8'))['records']
from collections import Counter
sin=[r for r in recs if (r.get('n') or '').strip()=='(sin denominación)']
print(len(sin), Counter(r['src'] for r in sin))"
grep -n 'sin denominación' tools/build_site_view.py   # linha 58
grep -c 'sin denominación' www/explorer/bic_data.json  # 99 linhas (JSON minificado: 1)
grep -o '\"card.noname\"[^,]*' site/public/i18n/pt.js

# 5. espaços duplos, controlo, NBSP, zero-width
python - <<'EOF'
import json, unicodedata
from collections import Counter
recs = json.load(open('www/explorer/bic_data.json', encoding='utf-8'))['records']
print('espaço duplo:', Counter(r['src'] for r in recs if '  ' in (r.get('n') or '')))
print('pontas:', sum(1 for r in recs if (r.get('n') or '')!=(r.get('n') or '').strip()))
cc = Counter()
for r in recs:
    for c in (r.get('n') or ''):
        if unicodedata.category(c) in ('Cc','Cf'): cc[f'U+{ord(c):04X}']+=1
print('controlo/formato:', dict(cc))
EOF
```
