From edaf776decd7b013121a93e021773cc1a89dadfa Mon Sep 17 00:00:00 2001 From: rafapolo Date: Wed, 30 Sep 2026 17:09:53 +0200 Subject: [PATCH] fix: filtros de autor e suporte limpam OCR ruim e viram select --- web/wall.html | 97 +++++++++++++++++++++++++++++++++------------------ 1 file changed, 63 insertions(+), 34 deletions(-) diff --git a/web/wall.html b/web/wall.html index b69d035..3f14dda 100644 --- a/web/wall.html +++ b/web/wall.html @@ -571,11 +571,7 @@
-
- - -
- +
@@ -1348,16 +1344,56 @@ function yearOf(meta) { return m ? parseInt(m[1], 10) : null; } +// ─── filter-value cleanup: the catalogue's type / author strings are messy ─── +// (OCR debris, case variants, life dates glued on, "Nome Sobrenome" vs "Sobrenome, Nome"). +// Only the filter lists and matching use these; the card still shows the raw record. +const KNOWN_TYPES = ['Fotografia/ Papel', 'Negativo/ Vidro', 'Diapositivo/ Vidro', 'Reprodução Fotomecânica/ Papel', 'Negativo flexível']; +function cleanType(raw) { + const s = String(raw || '').replace(/\s+/g, ' ').trim(); + if (!s) return ''; + const fold = deburr(s); + const known = KNOWN_TYPES.find(k => deburr(k) === fold); + if (known) return known; + // OCR debris: rebuild from the tail that survived ("…afia/ Papel", "…gativo/ Vidro") + if (/(?:rafia|FOa)\/ Papel$/.test(s)) return 'Fotografia/ Papel'; + if (/gativo\/ Vidro$/.test(s)) return 'Negativo/ Vidro'; + if (/Fotomec/.test(s)) return 'Reprodução Fotomecânica/ Papel'; + if (/[<>{}\d]/.test(s)) return ''; // unrecoverable garbage — leave it out of the list + return /[A-ZÀ-Ý]{3,}/.test(s) ? s.charAt(0) + s.slice(1).toLowerCase() : s; // "NEGATIVO flexível" → "Negativo flexível" +} + +const NO_AUTHOR = /^(s\/autor|s\/nome|s\/a|\[?s\.?n\.?\]?|sem autor|\[sem autor\]|autoria n[aã]o identificada|autor n[aã]o identificado|n[aã]o identificado|an[oô]nimo)$/i; +function cleanAuthor(raw) { + let s = String(raw || '').replace(/\s+/g, ' ').trim(); + s = s.replace(/^Fot[óo]grafo:\s*/i, '') + .replace(/\s*[-–]?\s*\((?:autoria|fot[óo]grafo|firma)\)/gi, '') + .replace(/\s+-\s+(?:Fot[óo]grafo|Est[úu]dio\/Ag[eê]ncia)$/i, ''); + // life dates / floruit glued to the end: ", 1864-1957", "(1893 - 1961)", "[? -1908]", "18- -19-" + const tail = /[,\s]*[\[(]?\s*(?:(?:ca\.|fl\.|m\.|n\.)\s*)?(?:\?|\d{2,4}-?)\s*(?:[-–]\s*(?:ca\.\s*)?\d{0,4})?\s*[\])]?[.,\s]*$/i; + for (let i = 0; i < 3 && tail.test(s) && s.replace(tail, '').length > 2; i++) s = s.replace(tail, ''); + s = s.replace(/[.,\s]+$/, '').replace(/^\[(.*)\]$/, '$1').trim(); + if (!s || NO_AUTHOR.test(s) || /[<>]|\d{3}/.test(s)) return ''; + return s; +} +// same person, different word order or accents → same key +const authorKey = name => deburr(name).replace(/[^a-z0-9 ]/g, ' ').split(/\s+/).filter(Boolean).sort().join(' '); + function setupFilters(mapData) { const entries = Object.values(mapData.map); - const instCounts = new Map(), typeCounts = new Map(), authorCounts = new Map(); + const instCounts = new Map(), typeCounts = new Map(), authorGroups = new Map(); let minYear = Infinity, maxYear = -Infinity; entries.forEach(e => { const meta = e.meta || {}; if (meta.community) instCounts.set(meta.community, (instCounts.get(meta.community) || 0) + 1); - if (meta.type) typeCounts.set(meta.type, (typeCounts.get(meta.type) || 0) + 1); - if (meta.contributor) authorCounts.set(meta.contributor, (authorCounts.get(meta.contributor) || 0) + 1); + const t = cleanType(meta.type); + if (t) typeCounts.set(t, (typeCounts.get(t) || 0) + 1); + const a = cleanAuthor(meta.contributor); + if (a) { + const k = authorKey(a), g = authorGroups.get(k) || { n: 0, forms: new Map() }; + g.n++; g.forms.set(a, (g.forms.get(a) || 0) + 1); + authorGroups.set(k, g); + } const y = yearOf(meta); if (y != null) { if (y < minYear) minYear = y; if (y > maxYear) maxYear = y; } }); @@ -1377,11 +1413,16 @@ function setupFilters(mapData) { selType.appendChild(opt); }); - const authorListEl = document.getElementById('f-author-list'); - [...authorCounts.entries()].sort((a, b) => b[1] - a[1]).slice(0, 300).forEach(([name]) => { + // one entry per person: the most common spelling names the group, and + // "Sobrenome, Nome" wins a tie over "Nome Sobrenome" + const authorNames = new Map(); + const selAuthor = document.getElementById('f-author'); + [...authorGroups.entries()].sort((a, b) => b[1].n - a[1].n).forEach(([key, g]) => { + const name = [...g.forms.entries()].sort((a, b) => b[1] - a[1] || b[0].includes(',') - a[0].includes(','))[0][0]; + authorNames.set(key, name); const opt = document.createElement('option'); - opt.value = name; - authorListEl.appendChild(opt); + opt.value = key; opt.textContent = `${name} (${g.n.toLocaleString('pt-BR')})`; + selAuthor.appendChild(opt); }); const decadeMin = Math.floor(minYear / 10) * 10; @@ -1400,8 +1441,6 @@ function setupFilters(mapData) { const yearReadout = document.getElementById('f-year-readout'); const yearFill = document.getElementById('f-year-fill'); const histEl = document.getElementById('f-hist'); - const authorInput = document.getElementById('f-author'); - const authorX = document.getElementById('f-author-x'); const searchInput = document.getElementById('f-search'); const searchX = document.getElementById('f-search-x'); const clearBtn = document.getElementById('f-clear'); @@ -1414,7 +1453,9 @@ function setupFilters(mapData) { tileMeshes.forEach(m => { const meta = m.userData.meta || {}; m.userData._year = yearOf(meta); - m.userData._author = deburr(meta.contributor || ''); + const a = cleanAuthor(meta.contributor); + m.userData._author = a ? authorKey(a) : ''; + m.userData._type = cleanType(meta.type); // one haystack per piece: every metadata field, not a chosen few // the handle is the map's key, not a field of meta — it still has to be // searchable (people paste a handle or its number straight into the box) @@ -1448,7 +1489,6 @@ function setupFilters(mapData) { } function apply() { - const q = deburr(state.author.trim()); // free text is AND across terms, so "rio 1900" narrows instead of widening const terms = deburr(state.search.trim()).split(/\s+/).filter(Boolean); const decCounts = new Array(nDecades).fill(0); @@ -1460,8 +1500,8 @@ function setupFilters(mapData) { // bars show what each decade *would* give under the other filters const okRest = (!state.institution || meta.community === state.institution) && - (!state.type || meta.type === state.type) && - (!q || d._author.includes(q)) && + (!state.type || d._type === state.type) && + (!state.author || d._author === state.author) && terms.every(t => d._hay.includes(t)); if (okRest && d._year != null) { const i = Math.floor((d._year - decadeMin) / 10); @@ -1494,7 +1534,6 @@ function setupFilters(mapData) { const active = activeFilters(); renderChips(active); searchX.hidden = !state.search; - authorX.hidden = !state.author; clearBtn.classList.toggle('disabled', active.length === 0); countBadge.classList.toggle('empty', active.length === 0); @@ -1522,7 +1561,7 @@ function setupFilters(mapData) { if (state.search.trim()) out.push({ key: 'search', text: `“${state.search.trim()}”` }); if (state.institution) out.push({ key: 'institution', text: state.institution }); if (state.yearTouched) out.push({ key: 'year', text: yearLabel(state.yearLo, state.yearHi) }); - if (state.author.trim()) out.push({ key: 'author', text: state.author.trim() }); + if (state.author) out.push({ key: 'author', text: authorNames.get(state.author) }); if (state.type) out.push({ key: 'type', text: state.type }); return out; } @@ -1531,7 +1570,7 @@ function setupFilters(mapData) { if (key === 'search') { state.search = ''; searchInput.value = ''; } if (key === 'institution') { state.institution = ''; selInst.value = ''; } if (key === 'type') { state.type = ''; selType.value = ''; } - if (key === 'author') { state.author = ''; authorInput.value = ''; } + if (key === 'author') { state.author = ''; selAuthor.value = ''; } if (key === 'year') resetYear(); apply(); } @@ -1572,17 +1611,7 @@ function setupFilters(mapData) { }); searchX.addEventListener('click', () => { clearTimeout(searchTimer); clearOne('search'); searchInput.focus(); }); - let authorTimer = null; - authorInput.addEventListener('input', () => { - authorX.hidden = !authorInput.value; - clearTimeout(authorTimer); - authorTimer = setTimeout(() => { state.author = authorInput.value; apply(); }, 160); - }); - authorInput.addEventListener('change', () => { // picking from the datalist - clearTimeout(authorTimer); - state.author = authorInput.value; apply(); - }); - authorX.addEventListener('click', () => { clearTimeout(authorTimer); clearOne('author'); authorInput.focus(); }); + selAuthor.addEventListener('change', () => { state.author = selAuthor.value; apply(); }); function onYear(e) { let lo = parseInt(loSlider.value, 10), hi = parseInt(hiSlider.value, 10); @@ -1600,9 +1629,9 @@ function setupFilters(mapData) { hiSlider.addEventListener('input', onYear); clearBtn.addEventListener('click', () => { - clearTimeout(authorTimer); clearTimeout(searchTimer); + clearTimeout(searchTimer); state.search = ''; state.institution = ''; state.type = ''; state.author = ''; - searchInput.value = ''; selInst.value = ''; selType.value = ''; authorInput.value = ''; + searchInput.value = ''; selInst.value = ''; selType.value = ''; selAuthor.value = ''; resetYear(); apply(); });