# Connector `ema` — European Medicines Agency, centrally authorised medicines (EU) | | | |---|---| | Source | https://www.ema.europa.eu/en/medicines/download-medicine-data — EMA "Download medicine data": the **medicines** table (every medicine evaluated under the centralised procedure, human and veterinary; one row per EMA product number) as an xlsx report | | Tier / category | 3 / regulatory (CLAUDE.md §13 — jurisdiction **EU**, authority **EMA**, `approval_type 'centralised'`; never a bare `approved = true`) | | Access | Bulk download, no authentication: `https://www.ema.europa.eu/en/documents/report/medicines-output-medicines-report_en.xlsx` (897,597 bytes, `application/vnd.openxmlformats-officedocument.spreadsheetml.sheet`) | | Docs verified | 2026-09-11 — download page + the live file with `User-Agent: CancerIndex/0.1`; EPAR URL pattern verified (HTTP 200) for xeljanz, keytruda, imfinzi | | License | EMA legal notice (https://www.ema.europa.eu/en/about-us/legal-notice) — `licenseStatus: approved`, `commercialUse: allowed`, `redistribution: attribution`; attribution: *"Source: European Medicines Agency (www.ema.europa.eu), medicines data download; © EMA, reproduced with acknowledgement"* | | Code | `packages/connectors/src/connectors/ema/` (`manifest.ts`, `normalize.ts`, `index.ts`, `ema.test.ts`, `fixtures/` incl. `build-fixtures.ts`); dependency `exceljs` (xlsx parsing) | ## Terms and licence (quoted) From the legal notice, section "European Medicines Agency copyright and limited reproduction notices" (verified 2026-09-11, stored verbatim in `manifest.termsNotes`): > Information and documents made available on EMA's webpages are public and may be reproduced and / or distributed, totally or in part, irrespective of the means and / or the formats used, for non-commercial and commercial purposes, provided that EMA is always acknowledged as the source of the material. Such acknowledgement must be included in each copy of the material. Citations may be made from such material without prior permission, provided the source is always acknowledged. The above-mentioned permissions do not apply to content supplied by third parties. The medicines table is EMA's own content (no third-party copyright is asserted on it). CancerIndex reproduces the EMA product number, status, dates, flags and the therapeutic-indication summary as a dated, sourced regulatory fact per medicine — never as a treatment recommendation. Update frequency (download page): *"The website automatically updates these data tables overnight."* The connector runs weekly (`0 5 * * 3`), one download per run (≈ 3 s end to end), 1 request/s, no documented rate limit. ## Verified file layout (2026-09-11) One sheet, **"Medicine"**. Row 1 = `Content type: | Medicine | Output automatically generated from content on www.ema.europa.eu | 11/09/2026 - 06:00` (the generation timestamp becomes `datasetVersion = ema-medicines-2026-09-11T06:00`); rows 2–8 empty; **row 9 = header**; data from row 10 — **2,732 rows** (2,339 Human, 393 Veterinary). Columns are matched by **normalized header text** (lower-cased, whitespace and embedded newlines collapsed — `"Species\n(veterinary)"`, `"Pharmacotherapeutic group\n(human)"`), never by position (`COLUMNS` in `normalize.ts`). A missing **required** column fails the run loudly (`SchemaDriftError: required column(s) missing …`); unknown columns are kept in `raw.extra` and reported; missing optional columns are warned. | Column (header) | Used as | |---|---| | `Category` | Human / Veterinary — only Human rows are mapped | | `Name of medicine` | medicine name → `drug_codes.ema_product.label`, `brand` alias (single-drug medicines), `raw.medicine` | | `EMA product number` | `EMEA/H/C/004214` → `application_number`, `drug_codes.ema_product`, source record id, edge `source_record_id` | | `Medicine status` | Authorised, Withdrawn, Application withdrawn, Refused, Lapsed, Opinion, Expired, Revoked, Suspended, Opinion under re-examination, Withdrawn from rolling review → status mapping below | | `International non-proprietary name (INN) / common name`, `Active substance` | substances (`;` / ` / ` separated; INN preferred, active substance = salt form fallback) | | `Therapeutic area (MeSH)` | `;`-separated MeSH descriptors → scope + `mesh_therapeutic_area` edges | | `ATC code (human)`, `Pharmacotherapeutic group (human)` | scope (L01/L02), `drug_codes.atc` (label = group) | | `Therapeutic indication` | official summary → `indication` (≤ 4,000 chars, full text in `raw.indicationFull`), cancer mapping | | `Accelerated assessment`, `Conditional approval`, `Biosimilar`, `Generic`, `Orphan medicine`, `PRIME: priority medicine`, `Advanced therapy`, `Exceptional circumstances`, `Additional monitoring` | Yes/No flags → `accelerated`, `conditional`, `raw.flags` | | `Marketing authorisation developer / applicant / holder` | `raw.holder` | | `Marketing authorisation date` | **`approval_date`** (initial authorisation) | | `European Commission decision date` | `raw.ecDecisionDate` only — it is the **latest** Commission decision (Xeljanz: 23/07/2026 vs authorisation 22/03/2017; for withdrawn medicines often the withdrawal decision), so it is never used as approval date | | `Withdrawal / expiry / revocation / lapse of marketing authorisation date`, `Suspension of marketing authorisation date` | `withdrawal_date` (never derived from anything else) | | `Last updated date` | `source_records.source_updated_at`, `provenance.updated_at_source` | | `Medicine URL` | `https://www.ema.europa.eu/en/medicines/human/EPAR/` → `provenance.source_url` (fallback: the download page) | Dates are `DD/MM/YYYY` (`emaDate`: malformed / impossible → null, never fabricated). ## Ingestion 1. **Download + anomaly guard**: one GET; < 50 KB or < 1,000 parsed rows → `anomaly:` error; `ctx.guardCount('medicine', rows)` against the previous successful run (first run = baseline). 2. **Raw layer**: `workbook` source record (the file bytes, base64 + sha256, keyed by dataset version — `--mode backfill` replays it without HTTP) and one `medicine` record per row (payload = the parsed row, every cell verbatim). Rows are processed sorted by product number; `ctx.cursor.lastProductNumber` resumes a run stopped by the time budget; pass bookkeeping (`passCount`) records `approval_added` / `approval_status_changed` change events from pass 2. 3. **Scope** (`classifyOncology`, human rows only) — a medicine is oncology when at least one holds: - **`atc_l01_l02`**: an `ATC code (human)` starts with L01 (antineoplastic) or L02 (endocrine therapy); - **`mesh_neoplasm`**: a MeSH therapeutic area that (a) carries neoplasm vocabulary (`ONCOLOGY_LABEL_RE`) and (b) resolves through `cancer_aliases` (`CancerResolver.byLabel`, MeSH inversion; `mesh_heading` aliases from the MeSH connector) to an active cancer entity. Generic descriptors (`Neoplasms`, `Cancer`, `Carcinoma`, `Neoplasm Metastasis`…) never count; benign conditions the ontology knows as entities (`Leiomyoma`, `Hemangioma`, `Prostatic Hyperplasia`, `Dupuytren Contracture`, `Amyloidosis`) fail (a) and are not oncology; - **`indication_text`**: the indication matches the ClinicalTrials.gov oncology vocabulary (`ONCOLOGY_LABEL_RE`) after neutralising two prose false positives: *tumour necrosis factor* (anti-TNF biosimilars) and the lower-case word *all* (the `\ball\b` abbreviation of ALL). Everything else is counted (`nonOncology`) and stays in the lake. **Cancer mapping and drug minting are only allowed** when the medicine is *oncology by class* (ATC L01/L02, or MeSH + oncology text), is **not a diagnostic** (ATC V04 / V08 / V09), and has at least one **therapeutic** sentence. Medicines in scope by indication text alone (denosumab for bone loss in prostate cancer, G-CSF, epoetins, fentanyl for cancer pain, antiemetics…) get an honest EU approval row when their substance is already a known drug, with `cancer_id NULL`, no edge and no minted drug. 4. **Substances → drugs** (deterministic, no LLM): the INN cell is split on `;` and ` / ` (never on commas — vaccine names; a `/` inside brackets is part of the name). Lookup in `drug_aliases.normalized` (in-memory index, kept in sync): the full label first, then the molecule with the salt form stripped (`parseIngredientName` shared with Health Canada: "abiraterone acetate" → abiraterone, "niraparib (tosilate monohydrate)" → niraparib). Exactly one drug → `ALIAS`; several → the `generic` alias, else the drug's own name, else **ambiguous** → `unresolved_labels` (`ambiguous_alias`: dacomitinib, trastuzumab deruxtecan on 2026-09-11 — two entities each). Biosimilars / generics (`Biosimilar` / `Generic` = Yes) resolve to the existing molecule (Herzuma, Ontruzant → Trastuzumab; Bortezomib Hospira → Bortezomib) — they never mint. - **Unknown INN, single substance, authorised or formerly authorised, oncology by class, therapeutic** → a drug is **minted** (`mintId 'DRUG'`; name = INN in Title Case — long common names with capitals, brackets or commas are kept verbatim; `slug` with `-ema-` on clash; `developmentStatus 'marketed_eu'` when authorised; aliases `generic`, `salt`, `brand`; change event `created`). - Otherwise → `unresolved_labels` with the reason: `component_of_combination`, `not_authorised_medicine` (refused / withdrawn application / pending opinion), `diagnostic_agent`, `scope_from_indication_text_only`, `scope_from_mesh_area_only`, `non_therapeutic_indication` (epoetin alfa, luspatercept, imetelstat, Gliolan), `no_inn_or_active_substance` (older withdrawn entries). - Rows are processed in product-number order, so a medicine whose INN is minted by a later row of the same pass (Prolia → denosumab minted by Yaxwer) is **retried** at the end of the pass: one run converges (`retried 103 … 2 gained rows` on the first run). 5. **Codes and aliases** per resolved drug: `drug_codes` `ema_product` (code = product number, label = medicine name) and `atc` (one per code, label = pharmacotherapeutic group), `match_type EXACT_IDENTIFIER`; a `brand` alias = medicine name without "(previously …)" for single-drug medicines only (a combination name is not an alias of one component). 6. **Status** (`mapEmaStatus`) — `Authorised` → **`approved`** (`conditional = true` when Conditional approval = Yes, `accelerated = true` when Accelerated assessment = Yes — a review track, noted in `raw.acceleratedNote`); `Withdrawn`, `Lapsed`, `Expired`, `Revoked`, `Suspended` → **`withdrawn`** with `withdrawal_date` from the explicit column (null otherwise; a suspension is reversible — the verbatim status stays in `raw.emaStatus` and `raw.emaStatusNote` says it is the medicine's, not the molecule's); `Refused`, `Application withdrawn`, `Withdrawn from rolling review`, `Opinion`, `Opinion under re-examination` → **no approval row** (a refusal or a pending opinion is not an approval; counted, lake only; any existing edge of that medicine is superseded). 7. **Approval row** per (medicine, drug) — `drug_approvals`: `jurisdiction 'EU'`, `authority 'EMA'`, `approval_type 'centralised'`, `application_number` = product number, `approval_date` = marketing authorisation date, `indication` = the official summary (≤ 4,000 chars, "…"), `raw` with everything above plus `scope`, `substances`, `flags`, `cancer_match`, `cancerCandidates`, `meshCancerIds`, `therapeutic`. Idempotent key `(source, drug, 'EU', product number)`: existing rows are updated in place, nothing is deleted. Provenance: one row per medicine and run when the source record changed (`sourceUrl` = EPAR page, `dataset 'EMA medicines data (xlsx)'`, `datasetVersion`, `evidenceType regulatory`, `accessLevel open`, `geography 'EU'`, `publishedAt` = approval date, `updatedAtSource` = last updated date, methodology text); an unchanged medicine whose rows exist reuses its previous provenance row. 8. **Cancer mapping** (`resolveIndication`, openFDA `CancerDictionary` / `resolveBullet` / `CancerReconciler` imported from `../openfda/normalize.js`): the indication is split into paragraphs, then `;` / `:` clauses and sentences (EMA lists indications as "…: as monotherapy for …; for treatment of …"). Sentences whose object is supportive care, prevention or diagnosis (`isSupportiveCareSentence`: "prevention of skeletal events", "treatment of symptomatic anaemia", "patients receiving chemotherapy", "active immunisation…", "visualisation of…", "vasomotor symptoms"…) are flagged and never yield a cancer; an introduction ending with ":" hands its nature to the clauses it introduces. Each remaining sentence → 0 / 1 cancer (`PROBABILISTIC`, several mentions collapse only along one lineage). The whole indication → one cancer → **`cancer_id`**; otherwise `cancer_id NULL` and the distinct cancers in `raw.cancerCandidates`. `tumor_agnostic` when a sentence pairs "solid tumours" with an agnostic biomarker. The dictionary is built **without** the build-time reconciler: for an alias shared by a parent and its child ("breast cancer" → Malignant Breast Neoplasm / Breast Carcinoma / Childhood Breast Carcinoma) the narrowest-of-lineage rule picked the child, so adult breast-cancer indications mapped to *Childhood Breast Carcinoma* (verified 2026-09-11); the resolver's preferred / display-name disambiguation decides instead. 9. **Knowledge edges** — for **authorised** medicines only: one `drug APPROVED_FOR cancer` edge per cancer named by a therapeutic sentence (`evidenceLevel 'EMA indication text'`), plus one per MeSH therapeutic area (`evidenceLevel 'mesh_therapeutic_area'`, only when the text did not already name that cancer) — `evidenceCategory regulatory_status`, `cancer_context_ids = [cancer]`, `provenance_ids`, `source_record_id` = product number. Edges of a medicine that is no longer authorised, or whose indication no longer names the cancer, are set to `status 'superseded'`. Multi-indication drugs therefore get one edge per cancer (Keytruda: 15 text edges + 4 MeSH). `dry_run` downloads, parses and maps without writing (25 sample lines + totals); `--mode backfill` replays the lake copy of the workbook (fallback: the per-row `medicine` payloads) — no HTTP, cursor untouched. Health check: `HEAD` on the xlsx must answer a spreadsheet content-type ≥ 100 KB. ## Observed runs (local copy of production, 2026-09-11) | | | |---|---| | Dry run | 2,732 rows (2,339 human, 393 veterinary) in 1.7 s; 620 human medicines in scope — ATC 418, MeSH 462, indication text 573 (text-only 111); out of scope 1,719 | | First run (`ING-EMA-20260911-000001`) | 3.0 s, 1 download. Statuses among scoped medicines: Authorised 446, Withdrawn 84, Application withdrawn 68, Refused 23, Lapsed 6, Opinion 5, Expired 2, Revoked 1, Opinion under re-examination 1. 622 substances: 475 by alias + 4 after salt stripping, **36 minted**, 2 ambiguous, 113 unresolved (queued), 13 medicines without INN. **459 EU approval rows** on 245 drugs / 456 medicines: 385 `approved` (31 conditional, 12 accelerated), 74 `withdrawn` (Withdrawn 66, Lapsed 5, Expired 2, Revoked 1; 48 with a withdrawal date), 457 with an approval date, **190 with `cancer_id`**, 4 tumour-agnostic, 71 flagged non-therapeutic (supportive care). **859 edges**: 529 `EMA indication text` (188 drugs × 95 cancers), 330 `mesh_therapeutic_area` (126 drugs × 56 cancers). 571 codes (459 `ema_product`, 112 `atc`), 187 brand + 36 generic + 4 salt aliases, 459 provenance rows, 2,732 + 1 source records (2.9 MB in the lake). Retry pass: 103 deferred medicines, 2 gained rows | | Second run (idempotency) | 2,733 source records unchanged, 0 created / 459 updated approval rows, 0 provenance rows, 0 change events | | Backfill | replayed `ema-medicines-2026-09-11T06:00` from the lake: same counts, 0 HTTP requests | | `pnpm cix reconcile-drugs` | linked trial interventions **78,105 → 78,983** (+878, all to the minted drugs: Thalidomide 236, Imiquimod 116, Denosumab 74, Serplulimab 69, Talimogene laherparepvec 43, Duvelisib 38, Tivozanib 32…) | Minted (36): 5-Aminolevulinic Acid, Alitretinoin, Bexarotene, Cinacalcet, Denosumab, Dinutuximab Beta, Duvelisib, Histamine, Hydroxycarbamide, Ibandronic Acid, Imiquimod, Imlunestrant, Linvoseltamab, Melphalan Flufenamide, Mifamurtide, Moxetumomab Pasudotox, Nogapendekin Alfa Inbakicept, Obecabtagene Autoleucel, Odronextamab, Padeliporfin, Pixantrone Dimaleate, Radium Ra223 dichloride, Serplulimab, Sugemalimab, Tagraxofusp, Talimogene Laherparepvec, Temoporfin, Thalidomide, Thyrotropin Alfa, Tisotumab Vedotin, Tivozanib, Toremifene, Vimseltinib, Vinflunine, two cell therapies kept under their EMA common name (sipuleucel-T "Autologous peripheral-blood mononuclear cells activated with…", Zalmoxis "Allogeneic T cells genetically modified…"). Most-connected cancers by EU text edges: NSCLC 33 drugs, malignant breast neoplasm 25, multiple myeloma 21, RCC 14, ALL 12, colorectal 11, follicular lymphoma 10, prostate 10, AML 9, melanoma 8. ## Limitations / notes - **Medicine-level granularity**: one row per (medicine, active substance) — biosimilars and generics each have their own row on the same molecule (trastuzumab: Herceptin + 6 biosimilars). The approvals feed counts medicines, not molecules; `raw.flags.biosimilar / generic` say which. - **Dates**: `approval_date` is the initial EU marketing authorisation of *that medicine* (a generic's date for a generic). The Commission decision date is the latest decision, kept in `raw` only. Suspensions carry no date in the file (2026-09-11) → `withdrawal_date` null. - **Refusals / pending opinions** are not approvals: no row, no edge; they are in the lake and counted in the run log (`refused`, `notAuthorised`, `pending`) and their unknown INNs are queued (`not_authorised_medicine`). Positive CHMP opinions awaiting the Commission decision (`Opinion`, 5 on 2026-09-11) appear on the next weekly run once authorised. - **Cancer mapping is conservative**: multi-indication medicines have `cancer_id NULL` (edges carry the per-cancer information); a sentence naming ≥ 2 unrelated cancers yields none. MeSH-derived edges target the MeSH heading's entity, often the broad "X Neoplasm" umbrella (`malignant = false` umbrella of benign + malignant) — hence the separate `evidenceLevel 'mesh_therapeutic_area'`. - **Gray zone kept, flagged**: medicines oncology by MeSH with a therapeutic sentence outside the cancer itself (Cinacalcet — hypercalcaemia of parathyroid carcinoma + hyperparathyroidism; Ibandronic acid — one authorised medicine with a non-supportive sentence; Thyrotropin alfa — radioiodine ablation adjunct) are minted with their honest indication text and MeSH edge only. Conversely luspatercept (MDS anaemia), imetelstat and epoetins are *not* minted because every sentence targets anaemia — they land in `unresolved_labels` (`non_therapeutic_indication`). - **Combinations**: a component whose INN is unknown is queued (`component_of_combination`, 16 on 2026-09-11); the row is linked to the components that resolved (Akeega → Niraparib and Abiraterone Acetate; Phesgo → Pertuzumab and Trastuzumab). - **Ambiguous aliases** (2): "dacomitinib" and "trastuzumab deruxtecan" each match two drug entities — a duplicate-entity question for `drug-duplicates.ts`, not for this connector. - **Veterinary** rows (393) are recorded in the lake and skipped. `Species`, `ATCvet` and the veterinary pharmacotherapeutic group are parsed for the lake only. - **Finding for `openfda`**: the same `CancerDictionary.build(rows, disambiguate, reconciler)` call with the reconciler collapses shared aliases to the child concept (adult "breast cancer" → Childhood Breast Carcinoma, "diffuse large B-cell lymphoma" → Childhood DLBCL); the EMA connector passes `undefined` at build time. Not changed in the openFDA connector by this work. - Fixtures: `fixtures/ema-sample.xlsx` (38 real rows — oncology L01/L02, biosimilars, a generic, withdrawn / lapsed / expired / revoked / suspended / refused / application-withdrawn / pending rows, MeSH-only and text-only rows, combinations, vaccines, veterinary rows; one marketing authorisation date corrupted to `99/99/2016`), `ema-header-drift.xlsx` (required column renamed), `ema-empty.xlsx` (header only). Regenerate with `pnpm exec tsx packages/connectors/src/connectors/ema/fixtures/build-fixtures.ts /tmp/ema.xlsx`.