Short answer
Turning a PDF report into a spatial system means recovering the geology trapped in prose, tables, and figures and rebuilding it as a structured, queryable dataset — usually in PostGIS. The work splits into extraction (text, tables, coordinates, and map figures pulled out of the document), georeferencing and geometry construction (turning extracted coordinates or digitized figures into real geometry in a known CRS), and loading with provenance (every value tagged with the document, page, and figure it came from). The biggest decision is made on the first page: is the PDF born-digital with selectable text, or a scan that needs OCR? Everything downstream depends on the answer.
Triage the PDF first
Open the PDF and try to select text in a paragraph.
- Born-digital (text selects, tables have real characters): you can extract text and tables programmatically with good fidelity. Coordinates, sample IDs, and assay tables come out clean.
- Scanned (selecting "text" selects an image, or grabs nonsense): the document is pixels. You need OCR (Tesseract, or a commercial engine) before any parsing, and you must budget for transcription errors — especially in coordinates, where a single misread digit moves a sample kilometres.
A quick check:
pdftotext -layout report.pdf - | head -40
If that prints readable text, you are in the born-digital path. If it prints little or garbage, treat it as scanned.
Stage 1 — Extract text, tables, and coordinates
For born-digital PDFs, separate the three kinds of content:
- Tables (drill collars, sample assays, station coordinates) are the highest-value targets.
CamelotandTabulaextract ruled and whitespace-delimited tables to CSV; Camelot'slatticemode handles bordered tables,streammode handles whitespace-aligned ones.
import camelot
tables = camelot.read_pdf("report.pdf", pages="12-18", flavor="lattice")
tables[0].to_csv("collars.csv")
- Inline coordinates in prose ("station at 46°12'30"N, 7°45'10"E") are best caught with regular expressions tuned to the report's notation, then converted to decimal degrees. Watch for mixed formats (DMS, decimal degrees, UTM eastings/northings) within one document — geology reports are notoriously inconsistent.
- Figures (maps, cross-sections) are images. Extract them at full resolution for the next stage;
pdfimages -all report.pdf fig_dumps embedded images, or render a page region at high DPI withpdftoppm -r 300.
Stage 2 — Georeference map figures
A map figure is a picture until it is tied to coordinates. Export the figure, find control points (graticule ticks, a scale bar plus a known landmark, labelled grid corners), and warp it:
gdal_translate -of GTiff \
-gcp 320 240 7.50 46.20 \
-gcp 1850 240 7.80 46.20 \
-gcp 320 1400 7.50 46.00 \
-gcp 1850 1400 7.80 46.00 \
figure_3.png figure_3_gcp.tif
gdalwarp -t_srs EPSG:4326 -r bilinear figure_3_gcp.tif figure_3_geo.tif
Four well-spread points and an affine warp suffice for a clean modern figure; for a distorted scan use -tps. Note the figure's stated scale and CRS — older reports often use a national grid or a local datum, and assuming WGS84 will introduce a datum shift of tens to hundreds of metres. Once georeferenced, digitize the features you need (units, faults, sample sites) as vector layers in QGIS.
Stage 3 — Build geometry and load into PostGIS
With coordinates in CSV, construct geometry explicitly. The two-step idiom is to set the SRID of the source coordinates and then transform to your working CRS — never confuse the two (ST_SetSRID only labels; ST_Transform actually moves coordinates):
CREATE TABLE collars (
hole_id text PRIMARY KEY,
easting double precision,
northing double precision,
src_epsg integer,
geom geometry(Point, 4326),
src_doc text,
src_page integer,
extracted date
);
-- coordinates were UTM 32N (EPSG:25832); store as WGS84
UPDATE collars
SET geom = ST_Transform(
ST_SetSRID(ST_MakePoint(easting, northing), 25832),
4326);
CREATE INDEX collars_gix ON collars USING GIST (geom);
Load the CSV with \copy or ogr2ogr:
ogr2ogr -f PostgreSQL "PG:dbname=geo" collars.csv \
-oo X_POSSIBLE_NAMES=easting -oo Y_POSSIBLE_NAMES=northing \
-a_srs EPSG:25832 -nln collars_raw
Repeat the pattern for sample sites, traverse lines, and digitized polygons from the figures. Now the report is queryable: you can ask which samples fall inside a digitized alteration zone, buffer drillholes, or join assays to collars by hole_id.
Provenance is not optional
The thing that makes a spatial system from a report trustworthy rather than just convenient is that every feature can be traced home. Carry, on every row, the source document, page, figure or table number, extraction method (OCR vs born-digital vs manual digitizing), and extraction date. When a number looks wrong six months later, those columns let you reopen the exact figure on the exact page and check — instead of re-doing the whole extraction or, worse, silently trusting a typo. Provenance columns cost almost nothing to add and repeatedly save the project.
Common pitfalls and why they happen
- Coordinates land in the ocean or the wrong hemisphere. Swapped easting/northing, or longitude/latitude reversed. UTM eastings are six digits, northings seven (northern hemisphere) — a quick magnitude check catches swaps.
- Systematic offset of tens of metres. A datum mismatch: the report used a local or older datum and you assumed WGS84. Identify the source datum and transform properly through
ST_Transform. - Garbled assay values. OCR misread digits in a scanned table. OCR numeric columns demand a second pass and validation against totals or known ranges.
- Tables extracted with merged or shifted columns. Camelot's mode was wrong for the table style. Switch between
latticeandstream, and always eyeball the CSV against the PDF. ST_SetSRIDused where transformation was needed. The geometry now claims a CRS it is not in. Set the true source SRID, thenST_Transform.
Validation
Plot every extracted point over the georeferenced source figure in QGIS — points must fall where the report shows them. Compare table row counts to the report ("Table 4: 142 samples" should yield 142 rows). Run a bounds check: every feature should fall inside the report's stated study area (ST_Within against a project boundary). Re-derive a few values by hand from the original page. Confirm geometry validity and that the spatial index exists (SELECT ST_IsValid(geom) ...).
Bathyl perspective
A report is a snapshot; a spatial system is something you can keep asking questions of. We rebuild reports into PostGIS with the source CRS handled explicitly and full provenance on every feature, so the data is both queryable and auditable back to the exact page it came from — and a future analyst never has to trust a number on faith.
Related reading
- From Consulting Output to Data Product
- Earth Data Products Explained
- Terrain Intelligence Platform Strategy
- Spatial data products