Alan Turing Institute · Data Study Group
A structured database of maritime incident reports from two investigating bodies - the UK Marine Accident Investigation Branch (MAIB) and the US National Transportation Safety Board (NTSB) - hosted for you as a standard PostgreSQL database. This page orients you: what you have, how it was built, how the tables fit together, and - most importantly - how far to trust each field. The data dictionary is the per-column reference.
Read access to a running PostgreSQL database, the source report PDFs, and the paperwork to make sense of both. No access to CHIRP's application, pipeline or APIs; everything you need is in the four pieces below.
A standard PostgreSQL instance your IT service hosts. Connect with any Postgres client and query it read-only.
Every table and column defined, with the provenance and trust of each field. The companion page.
The entity-relationship diagram - every table and every link between them, below.
Every MAIB and NTSB report the database was built from, supplied as
zip archives through your IT service. Match a document row to its
file with documents.filename.
It is ordinary PostgreSQL - three things worth knowing before you query:
passage_embeddings.embedding and
shield_code_embeddings.embedding are
vector(1024) columns; a client without pgvector reads
them as text. The match_shield_codes(passage_id, strategy,
limit) function does the cosine search server-side, so you do
not need one.
tsvector columns are generated. The
full-text search columns are computed automatically from their source
text and are read-only - query them for search, don't write them.
Each published report (a PDF, from MAIB or NTSB) is broken into sentence-level text plus the report's own stated incident particulars. MAIB reports are additionally linked to structured incident data - occurrences, vessels, affected persons - drawn from the MAIB Data Portal spreadsheets; NTSB has no equivalent spreadsheet source, so that layer is MAIB-only. An analysis pipeline then extracts findings from the text: safety issues, recommendations, and named entities with every mention resolved. Every finding is traceable back to the exact sentences of the source report. CHIRP's SHIELD taxonomy - 113 causal-factor codes in 24 categories under four layers - is loaded, and every passage is embedded so codes can be suggested, but no code has yet been matched to a finding.
documents.jurisdiction separates the two corpora:
'UK' for the 796 MAIB reports, 'US' for the
456 NTSB reports - 1,252 documents in all. The pipeline has been run
over all 1,252, so the database is populated end to end; what is absent
is listed under What is not included, and the one substantive
gap is that missing SHIELD link.
The database is populated end to end - every pipeline stage has run over the whole corpus. Five tables are empty, for three different reasons; know which before you build on them.
documents,
authors, sentences, and the
report-derived document_particulars /
document_vessels. Both corpora, split by
documents.jurisdiction.
occurrences, vessels,
affected_persons, affected_persons_ppe,
the report↔occurrence link on
documents.occurrence_id, and the vocabularies
taxonomy_terms / ppe_items. No NTSB
document carries an occurrence_id.
passages /
passage_sentences (one passage per finding),
safety_issues (11,254), recommendations
(4,127), recommendation_organisations and the
organisations they resolve to. Written by the record
pass over all 1,252 documents.
narrative_entities (545,649) and
narrative_entity_mentions (1,267,349), every mention
resolved to an entity and located by character span.
passage_embeddings (one
vector per passage), shield_code_embeddings (452),
passage_embedding_queue drained. Behind the
match_shield_codes suggestion function.
shield_codes / shield_code_categories /
shield_layers), narrative_entity_types
(14), embedding_models,
embedding_strategies.
passage_shield_codes - the missing link. Taxonomy
loaded and every passage embedded, but no code matched to a
passage yet.
analysis - a spare table for a future per-passage
pass. Nothing writes it.
legislation, safety_issue_legislation -
the legislation-attachment feature is not implemented.
author_identifiers - nothing populates it at this
stage.
The data flows through pipeline stages, and the tables mirror them. Provenance is preserved as a chain of links, not copied data: any finding traces to its passage, the passage to its sentences, each sentence to its document and position.
A report PDF - MAIB or NTSB - is registered as a
documents row and each page is rasterised to an image;
the report's own stated facts go to
document_particulars and
document_vessels, and
documents.jurisdiction records which corpus it belongs
to.
A vision LLM reads each page image and returns its text and
structure directly; deterministic passes then run on top (normalise
metadata, rejoin broken lines, repair lossy pages), stored as ordered
sentences. Lifecycle:
queued → extracting → extracted → verified.
MAIB Data Portal data is loaded independently:
occurrences → vessels → affected_persons, classified
via taxonomy_terms, linked to MAIB reports through
documents.occurrence_id. (MAIB only - NTSB
publishes no comparable dataset.)
Each report is read for the records it publishes - safety issues
(findings and general safety lessons, split by
record_type) and recommendations - each written with its
own passages row. A recommendation's
addressee is resolved into organisations.
Run over all 1,252 documents.
A pass reads each document's sentences for the named things it talks
about, writing one narrative_entities row per distinct
thing and one narrative_entity_mentions row per
mention, each located by character span and typed by how it refers.
Every passage is encoded under
BAAI/bge-large-en-v1.5 (1024 dimensions) into
passage_embeddings, and each SHIELD code under four
strategies into shield_code_embeddings. These feed the
match_shield_codes function - but nothing has yet
accepted a suggestion.
Two incident-data sources are kept apart and linked, not merged: the
report PDF's own account lives in document_particulars /
document_vessels (both corpora); the MAIB Data Portal
spreadsheet's account in occurrences / vessels /
affected_persons (MAIB only); the two join through a
nullable documents.occurrence_id FK - several reports can
point at one occurrence (e.g. an interim then a final). Tree-shaped
classifications (event type, ship type, place on board, injury,
deviation) are normalised into taxonomy_terms. The full
entity-relationship diagram:
The data dictionary is authoritative for table and column definitions; the diagram is the map, the dictionary the legend.
This is the point of the dictionary. The database shows you structure; it cannot tell you which values are source fact, which a model produced, and which a human has since confirmed. Every column is labelled with one of the five below - the first two both mean "the source said so", and differ only in which source: a report PDF, or the MAIB spreadsheet.
| Source | Source-document field - from the report PDF, MAIB or NTSB (content or file metadata). |
| MAIB | MAIB Data Portal field - from the MAIB spreadsheet extract, on the tables it feeds (occurrences, vessels, affected_persons and their vocabularies). UK incidents only. |
| Pipeline | Pipeline-derived, deterministic - computed on ingest, no model involved. |
| AI-U | AI-generated, unverified - produced by an LLM or NLP model, not confirmed by a human. |
| AI-V | AI-generated, human-verified - model output an analyst has confirmed. |
| System | System metadata - surrogate keys, timestamps, status flags, generated search columns, seeded reference data. |
A slashed label - Pipeline / AI-U - marks a column whose extractor is deterministic with a model fallback.
Two rules run throughout. Row-level verification: tables
with an is_verified flag hold AI-generated content whose
status varies per row - treat is_verified = true as AI-V,
everything else AI-U. Sentence text is extracted by a
vision LLM reading each page image, with deterministic passes on top, so
it is AI-U, qualified by the parent documents.status
(verified = analyst-confirmed).
is_verified = true and no
document is at status = 'verified'. Treat every AI-U column
as machine output no human has checked.
Read these before drawing conclusions from the data.
documents.jurisdiction: the corpora differ
in body, report format, date span and field coverage, so pooling them
silently mixes populations.
occurrences, vessels,
affected_persons and affected_persons_ppe
come from the MAIB Data Portal and cover UK incidents alone; no NTSB
document has an occurrence_id. Even on the MAIB side the
report↔occurrence link is partial: 58 of the 796 MAIB documents
carry one. Treat the occurrence tables as an incident dataset in their
own right, not as an enrichment of every report.
occurrence_id links only 58 reports to an occurrence -
just 61 of the 9,063 occurrences carry a publication_number
at all, because MAIB publishes a report for a small fraction of the
incidents it records. Any analysis joining report text to spreadsheet
incident attributes runs on those 58 documents.
is_verified = true on
safety_issues, recommendations and
passage_shield_codes - and no row anywhere carries it; all
1,252 documents sit at status = 'extracted', none at
verified. narrative_entity_mentions.confidence
is the extraction's own self-report, not a measured accuracy.
passage_shield_codes) has not been made. This is the main
analytical gap in the dataset.
is_contributory is set on 2,975 of
11,254 rows and is_addressed on 3,035;
recommendations.reference_number on 2,414 of 4,127 and
addressee on 3,159. Absence is a property of the source
reports, not a gap in extraction - do not impute the unfilled rows.
document_particulars and document_vessels
exist for every document, but fields are filled only where the report
states them, and NTSB reports state fewer. Of 456 NTSB / 796 MAIB
rows: accident_date 198 / 796,
accident_location 208 / 731, severity
71 / 305, accident_type 130 / 279,
loss_of_life 33 / 183;
document_vessels.port_of_origin and
destination are empty for NTSB (286 / 262 for MAIB),
while vessel_name is better covered for NTSB (400 / 299).
authors is not canonicalised. Its 22
rows carry PDF-metadata name variants of the two bodies
(MAIB, Marine Accident Investigation Branch,
www.maib.gov.uk; NTSB,
National Transportation Safety Board, the typo
NSTB) plus individual people named as a PDF's author. Use
documents.jurisdiction to identify the publishing body,
not authors.name.
documents.extraction_fallback = true - a degraded
result for some pages. 201 documents are flagged that way (182 MAIB,
19 NTSB).
status = 'verified' documents are
analyst-confirmed; extracted ones are not. As delivered
all 1,252 documents sit at extracted - none has been
analyst-verified. The source PDFs ship with the dataset, so anything
that matters can be checked against the original page.
document_particulars.accident_type / severity,
document_vessels.vessel_type) are free text - normalise
before counting, and note MAIB and NTSB word them differently, so the
free text is doubly heterogeneous. Identifiers differ too:
publication_number is a MAIB Publication_No
for UK reports and an NTSB report number (MAB-,
MIR-, MAR-) for US ones, and
document_type is a GOV.UK publication class that is null
for every NTSB document. Spreadsheet classifications are normalised into
taxonomy_terms and referenced by *_term_id.
Coordinates are verbatim text, not validated geo types.
Five tables are empty, for three different reasons:
passage_shield_codes - the missing link.
No matching pass has been run and no analyst has accepted a
suggestion, though everything the match needs is present.
analysis - a convenience table for
free-form per-passage output, so a future pass has somewhere to write
without a schema change. Nothing writes it today.
legislation and
safety_issue_legislation - attaching legislation
to a safety issue is a designed but unbuilt feature.
author_identifiers is likewise unpopulated.
Also absent: pipeline observability data (LLM traces, extraction logs) and any application authentication data.
As of 28 August 2026. Seeded vocabularies are loaded reference data.
| Table | Rows |
|---|---|
| documents | 1,252 (796 MAIB, 456 NTSB) |
| document_particulars | 1,252 |
| document_vessels | 1,252 |
| authors | 22 |
| author_identifiers | 0 (empty) |
| occurrences | 9,063 (MAIB only) |
| vessels | 9,830 (MAIB only) |
| affected_persons | 3,040 (MAIB only) |
| affected_persons_ppe | 924 (MAIB only) |
| taxonomy_terms | 410 (seeded) |
| ppe_items | 9 (seeded) |
| sentences | 341,057 (293,454 MAIB, 47,603 NTSB) |
| passages | 15,381 (13,892 MAIB, 1,489 NTSB) |
| passage_sentences | 15,939 (14,382 MAIB, 1,557 NTSB) |
| analysis | 0 (empty) |
| passage_shield_codes | 0 (empty - no code matched yet) |
| safety_issues | 11,254 (10,038 MAIB, 1,216 NTSB) |
| safety_issue_legislation | 0 (empty) |
| legislation | 0 (empty) |
| recommendations | 4,127 (3,854 MAIB, 273 NTSB) |
| recommendation_organisations | 3,658 (3,342 MAIB, 316 NTSB) |
| organisations | 802 (710 companies, 92 classes) |
| organisation_identifiers | 86 |
| narrative_entities | 545,649 (446,735 MAIB, 98,914 NTSB) |
| narrative_entity_mentions | 1,267,349 (1,032,182 MAIB, 235,167 NTSB) |
| shield_codes | 113 (seeded) |
| shield_code_categories | 24 (seeded) |
| shield_layers | 4 (seeded) |
| narrative_entity_types | 14 (seeded) |
| embedding_models | 1 (seeded) |
| embedding_strategies | 4 (seeded) |
| passage_embeddings | 15,381 (one per passage) |
| shield_code_embeddings | 452 (113 codes x 4 strategies) |
| passage_embedding_queue | 0 (drained) |