Moved from gov-agreg/src/pages/achizitii/* to root (drop prefix). - 22 pages migrated, 127 files total - All internal links: /achizitii/X → /X (176 occurrences fixed) - AchizitiiLayout subnav rewritten: /X paths, top-right link to vreaudigital.ro hub - BaseLayout new (vreau.digital branding, OG tags, site URL) - astro.config.mjs: site https://vreau.digital, server output (was static) - docker-compose: port 5096 (vreaudigital is 5095), container vreau-digital - deploy.sh: paths /opt/vreau-digital, log /var/log/vreau-digital-deploy.log Backend shared with gov-agreg: - PostgreSQL satra (same schemas: seap, firms, anaf, anre, ...) - Photon, Martin tiles - Infisical /vreaudigital path (DATABASE_URL etc. shared) build: PASS (npx astro check 0 errors, npm run build 5s vite + 10s server)
29 KiB
ANI Declarații de Avere și Interese — Ingest Plan
Mission: ingestăm 1.3M+ declarații PDF ale demnitarilor și înalților funcționari publici din România (2008–2022 + e-DAI 2022→) ca să cross-referențiem politicieni × firme deținute × contracte SEAP — flagship feature pentru vreaudigital.ro.
Cadru legal: Legea 176/2010 (publicarea declarațiilor e mandate-by-law, GDPR-safe). CNP-ul nu e public; tot restul (nume, funcție, instituție, valori, locații imobile, asocieri firme) este.
Status la 2026-05-09: arhitectură + schemă DB + scraper skeleton. Full ingest = 15 zile efort focalizat, nu se face în această sesiune. Acest document e foaia de drum pentru a continua "cold" în următoarea sesiune.
1. Pipeline (high-level)
┌──────────────────────────────────────────────────────────────────────────┐
│ SURSE (3 portaluri ANI, fiecare cu mecanică diferită) │
│ │
│ ▸ old-declaratii.integritate.eu JSF/IceFaces, search + CSV export │
│ (2008–2022 archive, ~12M docs, ~1.3M declaratii distincte) │
│ → /search.html?... POST forms, /DownloadServlet?fileName=…&… │
│ │
│ ▸ declaratii.integritate.eu Angular SPA + Spring Boot REST API │
│ (e-DAI 2022→, declarații electronice native) │
│ → /api/<form-id>/submission JSON cu data.bucket + data.filename │
│ │
│ ▸ depozitar.integritate.eu depozit raw, mirror partial │
│ (folosit ca fallback dacă portalul principal e down) │
└────────────────────────────────┬─────────────────────────────────────────┘
│
▼
┌──────────────────────────────────────────────────────────────────────────┐
│ STAGE 1 — Listing scraper (cron/scrape-ani-listings.sh) │
│ Walk results pages, populate ani.declaratii (URL + metadata only) │
│ Idempotent. Dedupe pe (official_name, year, declaration_type, source) │
│ Output: ~1.3M rows, ~120 MB postgres │
└────────────────────────────────┬─────────────────────────────────────────┘
│
▼
┌──────────────────────────────────────────────────────────────────────────┐
│ STAGE 2 — PDF download (cron/download-ani-pdfs.sh) │
│ Fetch PDFs sequential, store on satra disk │
│ Path: /opt/vreaudigital-data/ani/{year}/{sha256[:2]}/{sha256}.pdf │
│ Update ani.declaratii.pdf_path + raw_sha256 │
│ Estimat: 1.3M × ~300 KB avg = ~400 GB raw │
│ Throttled: 2 req/s → ~1 săpt 24/7 sau ~3 săpt @ 8h/zi │
└────────────────────────────────┬─────────────────────────────────────────┘
│
▼
┌──────────────────────────────────────────────────────────────────────────┐
│ STAGE 3 — PDF parser (src/parse-ani-pdf.ts) │
│ Two pipelines: │
│ (a) e-DAI (2022→): native text PDFs, generate de Form.io. │
│ → pdftotext -layout, regex pe câmpuri stabile. │
│ (b) Old (2008–2021): scanned + native mix. │
│ → pdftotext întâi; dacă < 50 caractere "vizibile" → OCR (tesseract │
│ cu lang=ron, ~5–15s/pagină pe satra). │
│ Template-detection: 3 generații de template-uri (2008–2010, 2011–2016, │
│ 2017+). Diferite în text labels dar structuri tabelare comune: │
│ I. Bunuri imobile, II. Bunuri mobile, III. Active financiare, │
│ IV. Datorii, V. Donații, VI. Conturi/depozite, VII. Plasamente, │
│ VIII. Funcții, IX. Asociații/firme deținute, X. Venituri. │
│ Output: structured rows în ani.bunuri, ani.shareholdings, ani.functii, │
│ ani.donatii. │
└────────────────────────────────┬─────────────────────────────────────────┘
│
▼
┌──────────────────────────────────────────────────────────────────────────┐
│ STAGE 4 — Entity resolution │
│ (a) Officials: dedupe across years pe (normalized_name + first │
│ institution + first year-of-birth slice). CNP-hash neavailable — │
│ omonimii rezolvate manual prin UI dacă apar conflicte SEAP. │
│ (b) Shareholdings: parsed firm_name (raw text din PDF) → CUI match │
│ via firms.match_company_name() (deja deployed în 019_cui_matcher). │
│ Tier 1: exact name match → 70% acoperire. │
│ Tier 2: pg_trgm similarity > 0.8 → +20%. │
│ Tier 3: manual review queue → 10% rest. │
└────────────────────────────────┬─────────────────────────────────────────┘
│
▼
┌──────────────────────────────────────────────────────────────────────────┐
│ STAGE 5 — UI surfacing │
│ ▸ /achizitii/politician/[slug] — profil demnitar (toate │
│ declaratiile, evolutie netto worth, firme deținute, contracte SEAP) │
│ ▸ /achizitii/firma/[cui] — adăugăm card "deținută de │
│ politicianul X" în profilul firmei existente │
│ ▸ /achizitii/retete/ │
│ politician-cu-firma-furnizor-stat │
│ (top 50 politicieni a căror firmă a încasat contracte SEAP) │
│ politician-uat-controleaza-furnizorul │
│ (primar/consilier × firma furnizor în UAT-ul lui) │
│ evolutie-avere-functie │
│ (politicieni cu cea mai mare creștere netto worth în mandat) │
└──────────────────────────────────────────────────────────────────────────┘
2. Schema DB (sql/030_ani_schema.sql)
5 tabele, schemă ani.*. Toate au (*)_at pentru audit + source_url ca să fim verifiable.
ani.officials — demnitari/funcționari publici
| col | type | note |
|---|---|---|
id |
bigserial PK | |
normalized_name |
text NOT NULL | lowercase + unaccent + collapse whitespace |
display_name |
text NOT NULL | "Popescu Ioan-Vasile" în casing original |
cnp_hash |
char(64) | SHA-256 al CNP dacă l-am extras (RAR — ANI maschează majoritar). Permite linkare across years fără a expune CNP. |
first_seen_year |
smallint | min(declaration year) |
last_seen_year |
smallint | max(declaration year) |
slug |
text UNIQUE | URL-friendly: "popescu-ioan-vasile" + suffix dacă collision |
created_at |
timestamptz default now() |
Index:
idx_officials_norm_namebtree pe normalized_nameidx_officials_norm_name_trgmgin pe normalized_name (trgm)idx_officials_slugunique
ani.declaratii — un PDF = un row
| col | type | note |
|---|---|---|
id |
bigserial PK | |
official_id |
bigint REFERENCES ani.officials(id) | nullable înainte de Stage 4 entity-resolution |
raw_official_name |
text NOT NULL | numele exact cum apare în portal (înainte de normalization) |
raw_institution |
text | "Ministerul X" / "Primaria Cluj-Napoca" / "Curtea de Apel Brasov" |
raw_function |
text | "Ministru" / "Consilier local" / "Judecator" |
raw_localitate |
text | localitatea declarată |
raw_judet |
text | județul |
year |
smallint NOT NULL | anul declarației (din date completare) |
declaration_type |
text NOT NULL CHECK (...) | 'avere' | 'interese' | 'avere+interese' |
submission_kind |
text | 'anuala' | 'numire-functie' | 'incetare-functie' | 'rectificativa' |
data_completare |
date | data completării declarate de demnitar |
source_portal |
text NOT NULL | 'old' | 'new' | 'depozitar' |
source_url |
text NOT NULL | URL public (dacă e old: DownloadServlet…; dacă e new: API submission ID) |
source_id |
text | ID intern al portalului (uniqueIdentifier la old, _id la new) |
pdf_path |
text | path relativ sub /opt/vreaudigital-data/ani/, NULL până la Stage 2 |
pdf_sha256 |
char(64) | hash conținut, dedupe |
pdf_size_bytes |
integer | |
fetched_at |
timestamptz | when PDF was downloaded |
parsed_at |
timestamptz | when parser finished |
parse_status |
text | 'pending' | 'ok' | 'ocr_required' | 'parse_failed' | 'template_unknown' |
parse_error |
text | last error message |
inserted_at |
timestamptz default now() |
Index:
idx_declaratii_official(official_id, year DESC) WHERE official_id IS NOT NULLidx_declaratii_year(year DESC, declaration_type)idx_declaratii_shaUNIQUE (pdf_sha256) WHERE pdf_sha256 IS NOT NULLidx_declaratii_sourceUNIQUE (source_portal, source_id)idx_declaratii_pending(parse_status) WHERE parse_status IN ('pending','ocr_required')idx_declaratii_raw_name_trgmgin pe raw_official_name
ani.bunuri — secțiunile I (imobile) + II (mobile)
| col | type | note |
|---|---|---|
id |
bigserial PK | |
declaration_id |
bigint NOT NULL REFERENCES ani.declaratii(id) ON DELETE CASCADE | |
category |
text NOT NULL CHECK (...) | 'imobil-teren' | 'imobil-cladire' | 'mobil-vehicul' | 'mobil-bijuterii' | 'mobil-altele' |
subcategory |
text | "agricol" / "intravilan" / "apartament" / "casa" / "auto" |
localitate |
text | judet/țara/localitate text |
judet |
text | județ-normalizat unde aplicabil |
tara |
text | implicit "România" |
year_acquired |
smallint | anul dobândirii |
mode_acquired |
text | "cumparare" | "mostenire" | "donatie" | "constructie" |
area_sqm |
numeric | suprafață în m² (terenuri/clădiri) |
share_pct |
numeric | cota-parte (1.0 = integrală) |
co_owner |
text | numele co-proprietarului dacă declarat |
value_lei |
numeric | valoarea declarată |
value_currency |
text default 'RON' | uneori EUR/USD |
raw_row_text |
text | textul brut din PDF, ca audit trail |
Index: idx_bunuri_decl (declaration_id), idx_bunuri_judet (judet) WHERE judet IS NOT NULL.
ani.shareholdings — secțiunea IX (firme deținute) + secțiunea VIII partial (asociat) — flagship table
| col | type | note |
|---|---|---|
id |
bigserial PK | |
declaration_id |
bigint NOT NULL REFERENCES ani.declaratii(id) ON DELETE CASCADE | |
firm_name_raw |
text NOT NULL | textul brut din PDF |
firm_cui |
text | rezolvat în Stage 4, NULL în primă fază |
firm_match_score |
real | similarity la match |
firm_match_method |
text | 'exact_name' | 'trgm' | 'manual' | 'unmatched' |
role |
text | "actionar" | "asociat" | "membru CA" | "administrator" | "cenzor" | "membru AGA" |
share_pct |
numeric | cota deținută (dacă declarată) |
value_lei |
numeric | valoarea participațiunii |
category |
text | 'societate' | 'asociatie' | 'fundatie' | 'cooperativa' | 'altele' |
raw_row_text |
text | audit |
Index:
idx_share_decl(declaration_id)idx_share_cui(firm_cui) WHERE firm_cui IS NOT NULLidx_share_name_trgmgin pe firm_name_raw
ani.functii — secțiunea VIII (funcții deținute, public + privat)
| col | type | note |
|---|---|---|
id |
bigserial PK | |
declaration_id |
bigint NOT NULL REFERENCES ani.declaratii(id) ON DELETE CASCADE | |
is_public |
boolean | TRUE = funcție în instituție publică |
function_name |
text NOT NULL | "Consilier", "Ministru", "Director general" |
institution_name |
text NOT NULL | numele instituției / firmei |
institution_cui |
text | rezolvat în Stage 4 (joinable cu firms.entities sau seap.cui_authority) |
start_year |
smallint | |
end_year |
smallint | NULL dacă activă |
salary_lei |
numeric | venit anual din această funcție (când declarat) |
raw_row_text |
text |
Index: idx_functii_decl (declaration_id), idx_functii_inst_cui (institution_cui) WHERE institution_cui IS NOT NULL.
ani.donatii — secțiunea V (donații primite)
| col | type | note |
|---|---|---|
id |
bigserial PK | |
declaration_id |
bigint NOT NULL REFERENCES ani.declaratii(id) ON DELETE CASCADE | |
donor_name |
text | cine a făcut donația |
donation_type |
text | 'bani' | 'imobil' | 'mobil' | 'servicii' |
value_lei |
numeric | |
currency |
text default 'RON' | |
year_received |
smallint | |
raw_row_text |
text |
Index: idx_donatii_decl (declaration_id).
3. Estimări de volum
| Stage | Estimat | Notă |
|---|---|---|
| officials (distinct) | ~150K | demnitari + magistrati + înalți funcționari activi în 2008–2025 |
| declaratii (rows) | ~1.3M | 8–10 declarații/persoană în medie pe carieră |
| pdf storage | ~400 GB | 300 KB avg × 1.3M |
| bunuri (rows) | ~6M | 4–5 bunuri/declarație medie |
| shareholdings (rows) | ~800K | doar 30–40% au firme declarate |
| functii (rows) | ~3M | 2–3 funcții/declarație |
| donatii (rows) | ~250K | rare (10–20% au donații) |
DB size estimat: ~12 GB (fără PDF-uri, doar metadata + parsed).
Cross-source magic queries posibile după ingest:
ani.shareholdings JOIN firms.entities ON cui JOIN seap.announcements ON supplier_cui→ politicianul X are firma Y care a câștigat 50M lei contracte.ani.functii(institutie publica) JOIN seap.announcements(authority) ON cui→ consilier local × autoritatea unde votează.- Year-over-year diff pe
ani.declaratii.bunuri→ creștere bruscă de avere în mandat.
4. Plan de execuție 15 zile
Faza 1 — Listing & metadata (Days 1–2)
- Day 1: Scraper pentru old portal (JSF/IceFaces). Reverse-engineer formul
/search.htmlcu pagination prin "Cautare avansata" + date range slicing (lună de lună 2008–2022 ca să nu lovim limita de rezultate). Output: ani.declaratii cu source_url + metadata, fără PDF-uri. Test: 1000 rows pe februarie 2020. - Day 2: Scraper pentru new portal (Angular SPA → Spring Boot REST). Reverse-engineer endpoint-ul
/api/<form-id>/submissioncu request real captured din DevTools (TODO: necesită browser session pentru a observa traffic). Test: 100 rows e-DAI 2024.
Deliverable Day 2: ~50K rows în ani.declaratii (sample), 0 PDF-uri downloaded.
Faza 2 — PDF download (Days 3–4)
- Day 3:
cron/download-ani-pdfs.shcu rate limit 2 req/s + retry exponential. Storage la/opt/vreaudigital-data/ani/{yyyy}/{sha256[:2]}/{sha256}.pdf. Update declaratii.pdf_path + sha + size + fetched_at. Run pe 1000 PDFs pilot. - Day 4: Scale-up. Background detached docker container, log la
/var/log/vreaudigital-ani-pdfs.log. Lasă să meargă în paralel cu munca pe parser.
Faza 3 — Parser PDF (Days 5–7)
- Day 5: Setup
pdftotextîn container + helper Nodesrc/parse-ani-pdf.ts. Detect template (2008-2010 / 2011-2016 / 2017+ / e-DAI). Parse secțiunea I (imobile) ca proof-of-concept. Test pe 10 PDFs din fiecare era. - Day 6: Parser secțiunile II (mobile) + IX (shareholdings). Acestea sunt cheia. Output în ani.bunuri și ani.shareholdings. Test pe 100 PDFs.
- Day 7: Secțiunile VIII (functii) + V (donatii). OCR fallback (tesseract ron) pentru PDF-uri scanate (estimat 15-25% din 2008-2014). Marcăm
parse_status='ocr_required'și rulăm OCR într-un cron separat.
Deliverable Day 7: parser care procesează ~70% din PDF-uri auto, ~25% cu OCR, ~5% template-unknown (manual review).
Faza 4 — Entity resolution (Days 8–10)
- Day 8: Officials dedup. SQL function
ani.dedup_officials()care grupează ani.declaratii pe (normalized_name + raw_judet + first-year). Manual review pentru top 1000 ambiguous (UI viewer simplu). - Day 9: CUI matching pentru shareholdings. Refolosim
firms.match_company_name()din 019_cui_matcher. Tier 1 exact + Tier 2 trgm > 0.8. Restul → tabel ani.shareholdings_unmatched_queue pentru review. - Day 10: CUI matching pentru functii.institution_cui. Authority-side: lookup în
seap.cui_authority. Private-side: lookup în firms.entities.
Deliverable Day 10: ~85% din shareholdings au CUI rezolvat → joinable cu seap și firms.
Faza 5 — UI (Days 11–13)
- Day 11:
/achizitii/politician/[slug]— pagină profil. Cards: declarații (timeline), evoluție avere, top firme deținute, contracte câștigate de firmele lui prin SEAP. Endpoint API la/api/politician/[slug]. - Day 12: Cross-link în pagina existentă
/achizitii/firma/[cui]: section "Asociat cu politicieni (declarații ANI)" — list de officials cu link la profil. - Day 13: Recipe page
politician-cu-firma-furnizor-stat. Top 50 politicieni unde COALESCE(firma.contracte_seap_total) > 0. Plus 2 recipe variants (evoluție avere, primar × furnizor UAT).
Faza 6 — Polish (Days 14–15)
- Day 14: Materialized views pentru perf:
mv_official_seap_exposure(politician → total contracte SEAP firme proprii), refresh nightly. Indexes finali. Analyze. - Day 15: Testing. Edge cases: persoane omonime (Popescu Ion × 50), firme cu nume identice cu funcții ("ASOCIAȚIA"), declarații fără PDF rezolvabil, OCR errors (CUI = "S.C. SRL" → garbage). Documentare pentru următoarea sesiune. Disclaimer GDPR în pagina /despre.
5. Risk register
| Risc | Probabilitate | Impact | Mitigare |
|---|---|---|---|
| Anti-scraping (rate limits, IP block) | Medie | Mare | User-Agent identifier ("gov-agreg/1.0 vreaudigital.ro contact:..."), 2 req/s max, retry exponential, fallback la depozitar.integritate.eu. ANI nu are istoric de a bloca scraperi (briatte/integritate a mers fără probleme 2017-2019). |
| PDF template change mid-corpus | Mare | Medie | Detector explicit per-template (regex pe header text); marker parse_status='template_unknown' pentru manual review. Quarterly check. |
| OCR errors → CUI invalid | Mare | Medie | Validare CUI cu checksum oficial (algoritm pe ultima cifră). Multe vor pica; tier 3 manual queue. |
| Name disambiguation (omonimii) | Mare | Mare | Default conservativ: NU merge officials cu nume identic dacă funcție/judeţ diferă. UI marker "posibil aceeași persoană" cu disclaimer. |
| GDPR challenges | Mică | Mare | Tot ce publicăm are basis legal (Legea 176/2010). Disclaimer prominent. NIMIC din CNP/data nașterii nu apare în UI. Privacy policy explicit. Right-to-rectify accesibil prin /contact. |
| Old portal sunset | Mare (anunțat 2025) | Mare | Prioritate: ingestăm rapid old portal înainte de takedown. Cache local PDF-uri ca single source of truth. New portal e SPA fragil → backup. |
| Volum PDF (400 GB) | Medie | Medie | Storage pe satra: avem ~2 TB free. Compress PDFs (zstd -19) la cold storage după parsing → ~120 GB. |
| Effort > 15 zile | Mare | Medie | MVP shippable la Day 13 (UI + recipe), zilele 14-15 sunt polish. Faza 4 (entity resolution) e cea mai imprevizibilă; dacă pică, ship cu shareholdings unmatched + UI care arată "candidat firmă declarată: X (nu am putut face matching automat)". |
6. Decizii de arhitectură (locked-in)
- Storage PDFs: filesystem pe satra, NU în Postgres bytea. Path templated pe sha256. Permite rsync/backup separat.
- Officials sunt dedupliated DUPĂ ce avem PDFs parsed, nu înainte. ani.declaratii.official_id e nullable înainte de Stage 4.
- CNP nu se stochează în clear, doar hash dacă e parsed (rar — ANI maschează în majoritatea cazurilor). Folosim doar pentru disambiguation, nu pentru afișare.
- Două scrapers separate (old + new), nu unul unificat. Mecanicile sunt prea diferite (JSF vs REST). Schema DB unificată via source_portal column.
- Parser e batch, nu online. Rulează nightly via cron. Nu blocăm scraper-ul de listing.
- Recipe registration: slot
politician-cu-firma-furnizor-statadăugat acum în RECIPES (returnează empty rows până avem date) — keeps URL stabil pentru SEO și menționabil în comunicare publică ("vine în curând").
7. Open questions (de rezolvat în sesiune următoare)
- e-DAI API endpoint exact: trebuie capturat din DevTools într-o sesiune browser reală (Selenium / Playwright). Bundle-ul SPA îl construiește runtime din config necunoscut. Plan: rulăm un browser headless 1x să capturăm 2-3 cereri și să reverse-engineerăm.
- Old portal CSV export: există un buton "Exporta resultate" — dacă funcționează prin POST simplu, sărim peste paginare HTML și luăm CSV bulk. Trebuie verificat manual.
- Tesseract pe satra: confirma că modelul
rone instalat. Estimat 5-15s/pagină pe CPU; pentru 200K PDFs OCR-required = 2-3 zile la concurrency 8. - Slug uniqueness pentru politicieni cu nume identice: Popescu Ion poate fi 50 de oameni. Strategy:
nume-prenume-judet-functie-prima-aparitie? Vezi după dedupe.
8. API endpoints discovered (live verification 2026-05-09)
Old portal (PRIMARY ingestion target)
https://old-declaratii.integritate.eu/search.html
JSF/IceFaces, POST cu form data:
form=form
form:searchKey_input=<query> # nume sau institutie
form:searchField_input=numePrenume # | "institutia"
form:submitButtonSS=cauta
javax.faces.ViewState=<grabbed-from-GET-search.html>
Response: HTML cu <table> rezultate, fiecare rând conține DownloadServlet?fileName=<X>.pdf&uniqueIdentifier=NTNTARTLNE_<NUM>.
Pattern fileName: <unique_id>_<persona_id>_<seq><suffix>.pdf unde suffix _a = avere, _b = interese (probabil; de validat pe corpus mai mare).
Coloane în tabel: Nume Prenume / Institutie / Functie / Localitate / Judet / Data completare / Tip declaratie / Vezi declaratie / Distribuie.
Pagination: form:resultsTable_pageInput, form:resultsTable_pageButton — JSF AJAX. Soluție: date range slicing (lună de lună) ca să nu lovim limita de pagini.
No auth, no captcha, no rate limit explicit. Confirmed working 2026-05-09.
New portal (e-DAI 2022→) — captcha protected
https://depozitar.integritate.eu/api/formio/grid/documente/submission
JSON REST API. Filtre cunoscute (Form.io syntax):
data.numePrenume__regex=<text>data.institutie__regex=<text>data.judet__regex=<JUDET-uppercase>data.functie__regex=<text>data.tipDeclaratie__regex=<text>data.dataCompletarii__gte=<ISO>,data.dataCompletarii__lte=<ISO>data.show__regex=1(filtru de bază pentru declarații publicate)sort=-created,limit=N,skip=N
Returnează 401 fără token Cloudflare Turnstile (x-jwt-token header). Necesită browser headless (Playwright) sau solver. Punem în Phase 2 zile 8-9 dacă merită.
Per-document: data.bucket + data.filename → API download via separate endpoint (TBD, capturat din browser session).
Depozitar.integritate.eu
Mirror al new portal-ului, aceeași API + Turnstile. Folosit ca fallback când portalul principal e down.
9. Sample PDFs analizate (Task 2)
5 PDFs descărcate de pe old-declaratii (stocate în satra:/tmp/ani_samples/):
| # | Persoană | An | Tip | Producer | Pages | Bytes | OCR? |
|---|---|---|---|---|---|---|---|
| 1 | KLAUS WERNER IOHANNIS (Președintele României) | 2024 | avere | iText 5.5.13.2 | ~5 | 60 KB | nu |
| 2 | KLAUS WERNER IOHANNIS | 2017 | avere | iText (similar) | ~5 | 60 KB | nu |
| 3 | KLAUS WERNER IOHANNIS | 2014 | avere | (no producer) | scanned | 293 KB | DA |
| 4 | EMIL BOCA (politist penitenciare Gherla — homonim cu Boc) | 2024 | avere | Kodak Capture | scanned | 58 KB | DA |
| 5 | CATALIN PREDOIU (Vice prim-ministru) | 2024 | avere | Alaris Capture | scanned | 112 KB | DA |
Observații cheie
-
Native vs scanned NU e funcție de an — depinde de cum a încărcat funcționarul. Iohannis 2024 e iText nativ (probabil generat din formular electronic intern), Predoiu 2024 e Alaris scanat (a printat → semnat → scanat). În practică: ~30-50% din PDF-uri necesită OCR independent de an.
-
Toate PDF-urile native au structură IDENTICĂ — același template iText cu secțiunile I-X marcate cu litere romane. Layout tabular cu 6-7 coloane pentru fiecare secțiune.
pdftotext -layoutpăstrează structura suficient cât regexes per-section funcționează. -
CNP e mascat în native PDFs (
*************) → nu vom putea extrage CNP-uri pentru disambiguation. Ne bazăm pe(name + institutie + judet + first_year). -
Localitate / Adresa sunt parțial mascate (
***********) pentru proprietăți → confirmă conformitatea ANI cu GDPR (adresă completă nu e public). Avem judeţul. Suficient pentru cross-check. -
Sample text extras (Iohannis 2024 secțiune I.2 Clădiri):
Tara: ROMANIA Judet: Sibiu Localitate: Sibiu Adresa: *********** Categorie: Apartament Anul dobândirii: 1997 Suprafata: 84.60 m2 Cota-parte: 1/1 Modul de dobândire: Contract de vânzare cumpărare Titularul: IOHANNIS CARMEN, IOHANNIS KLAUS→ toate câmpurile pentru
ani.bunurirezolvabile cu regex per-tabel. -
Filename suffix decoding (preliminary):
_a.pdfla sfârșit → declarație avere (confirmed pe Iohannis 2024 + 2017)_b.pdfla sfârșit → declarație interese (de validat)_NNN.pdf(3 cifre) la sfârșit → variantă numerotată (rectificative? batch upload?)
Recomandare parser
Strategie pe 3 nivele:
-
Tier 1: pdftotext -layout + regex per-secțiune (rapid, ~50 ms/PDF). Se aplică tuturor PDFs.
- Dacă output > 500 chars vizibili (nu doar headere) → procesăm.
- Folosim markeri "I. Bunuri imobile", "II. Bunuri mobile", "VIII. ", "IX. " ca anchor pentru extragerea blocurilor de text.
-
Tier 2: detect scanned + OCR (lent, ~5-15s/PDF). Aplicat când Tier 1 returnează < 500 chars.
tesseract <pdf-img> - -l ronîn container. PDF → imagine viapdftoppm -r 200întâi.- Output mai zgomotos: regex relaxat, mai mulți falși pozitivi.
-
Tier 3: template_unknown (ratele PDF-uri parsate nu match niciun template). Coadă manuală review în UI admin.
Tools:
- pdftotext (poppler-utils) + Node.js — nu Python (
pdfplumberar fi mai elegant dar adaugă dependency Python într-un repo TS). - tesseract-ocr-ron — în container alpine cu
apk add tesseract-ocr tesseract-ocr-data-ron. Estimat 5-15s/PDF pe satra CPU. 200K PDFs scanate × 8s = 18 zile single-thread → cu concurrency 8 = ~3 zile. - NO Apache Tika — overkill, mai bine pdftotext direct.
Effort: parser MVP la 70% acuratețe e ~3 zile (Day 5-7). Restul de 30% (template-uri vechi 2008-2010, edge cases) ajunge la 90% în următoarea iterație.