WKT (Well-Known Text) — OGC and ISO Text Standard for Geometries in SQL

2026-05-3112 min read
WKTOGCISO 19125PostGISSQLgeometryWKB

WKT, or Well-Known Text, is a text representation of geometries that OGC introduced in 1999 specifically so databases could conveniently store and exchange geospatial objects inside SQL queries. If in any PostGIS tutorial you see the string POINT(30.32 59.93) or POLYGON((...)) — that's WKT in its pure form. The format became so universal that it's supported by PostGIS, MySQL Spatial, Oracle Spatial, Microsoft SQL Server, SQLite/SpatiaLite, DuckDB, and every Python library from the Shapely / GeoPandas stack. WKT is not a "file container" like GeoJSON or Shapefile, but rather a common language databases, ETL pipelines, and spatial libraries use to talk to each other. In OSM2CDR wkt_exporter.py outputs a plain text file where each feature is one line with tags and a WKT geometry, ready to COPY into PostGIS or ingest in any ETL.

Map generation of Yaroslavl city center

History: OGC SFS 1999 → ISO 19125 → WKT 2 (ISO 19162, 2019)

WKT's story starts with a 1990s problem: every geographic information system stored geometries its own way. ESRI shapefiles, Intergraph GeoMedia, MapInfo TAB, Oracle Spatial — all of them were incompatible even at the level of describing "a polygon with a hole". Moving data between systems meant writing a custom converter.

In May 1999 the Open Geospatial Consortium (then OpenGIS Consortium) published the OGC Simple Features for SQL 1.1 specification (document 99-049). It described what functions a SQL DBMS must have to be called "spatial-enabled": ST_AsText, ST_GeomFromText, ST_Within, ST_Intersects, and dozens of others. Along with that, it introduced two serialization formats:

  • WKT (Well-Known Text) — human-readable text representation: POINT(x y), LINESTRING(...), POLYGON((...)).
  • WKB (Well-Known Binary) — compact binary representation with the same types but in binary form for efficient storage and fast parsing.

The idea was simple: if all vendors implement ST_AsText and ST_GeomFromText, any geometry can be pulled from any database as text, passed to another database, and stored back. WKT became the lingua franca for migrations between Oracle Spatial, IBM DB2 Spatial Extender, and the then-young PostGIS (version 0.1 — May 2001, founded by Refractions Research specifically around OGC SFS).

In 2004 ISO republished the specification as ISO 19125-1:2004 — "Geographic information — Simple feature access — Part 1: Common architecture". That cemented WKT and WKB as international ISO standards, not just OGC recommendations. Part 2 (ISO 19125-2) described SQL binding.

The standard was further updated in 2010 (OGC 06-103r4 — adding 3D and Measure coordinates: POINT Z (x y z), POINT M (x y m), POINT ZM (x y z m)), but the syntax stayed backward-compatible. Any WKT file from 1999 is readable by modern PostGIS without change — a rare achievement in an industry where specs usually break compatibility every five years.

A separate story is WKT 2 for coordinate reference systems. In 2015 OGC published standard 12-063r5, and in 2019 — ISO 19162:2019, describing a text format for CRS, not geometries. This is a completely different WKT — it has nothing to do with POINT(...) and POLYGON(...). WKT 2 replaces the old PROJ.4 strings and Esri .prj files: instead of +proj=longlat +datum=WGS84 +no_defs you write a structured block GEOGCRS["WGS 84", DATUM[...], CS[...], ID["EPSG", 4326]]. PROJ 6.0+ (2019), GDAL 3.0+, and PostGIS 3.0+ moved to WKT 2 as the primary way to describe CRS. The long text string you see in the srtext column of spatial_ref_sys today is WKT 2.

These two WKTs — for geometries (1999) and for CRS (2019) — are often confused. In the context of OSM2CDR and wkt_exporter.py we always mean the first one: text representation of points, lines, and polygons.

Inside WKT: geometry types and syntax

Seven base types from OGC SFS 1.1:

  • POINT — single point: POINT(30.32 59.93).
  • LINESTRING — sequence of connected points: LINESTRING(30.30 59.93, 30.32 59.94, 30.34 59.94).
  • POLYGON — closed ring (optionally with holes): POLYGON((0 0, 4 0, 4 4, 0 4, 0 0)). Interior rings go in separate parentheses: POLYGON((0 0, 10 0, 10 10, 0 10, 0 0), (3 3, 7 3, 7 7, 3 7, 3 3)).
  • MULTIPOINT — set of points: MULTIPOINT((1 2), (3 4)).
  • MULTILINESTRING — set of lines: MULTILINESTRING((0 0, 1 1), (2 2, 3 3)).
  • MULTIPOLYGON — set of polygons.
  • GEOMETRYCOLLECTION — heterogeneous collection: GEOMETRYCOLLECTION(POINT(1 2), LINESTRING(0 0, 1 1)).

OGC 06-103r4 added three variants with extra coordinates: Z (height), M (measure), ZM (both): POINT Z (30.32 59.93 12.5), POINT ZM (x y z m).

Coordinates are space-separated, points are comma-separated, polygon rings sit in their own parentheses. No restrictions on number encoding (integer, decimal, scientific — all allowed). UTF-8 is not required — WKT is plain ASCII, it has no free-text fields.

EWKT (Extended WKT) — PostGIS extension that adds a SRID prefix: SRID=4326;POINT(30.32 59.93). Not part of the OGC standard, but de facto became a second standard — supported by PostGIS, SpatiaLite, GDAL, Shapely. When you run SELECT ST_AsEWKT(geom) FROM ... in PostGIS, the output is exactly EWKT with SRID.

WKT vs WKB

WKB (Well-Known Binary) is the binary twin of WKT from the same OGC SFS 1.1. Same geometry types, but in binary representation: 1 byte endianness, 4 bytes type, 8 bytes per coordinate as IEEE 754 double. Moscow's polygon in WKT — around 3 MB ASCII, the same polygon in WKB — around 800 KB binary bytes. WKB parsing is 5–20x faster than WKT because no lexical scan is needed.

In PostGIS the internal geometry representation is a WKB variant (specifically EWKB with SRID and Z/M flags). When you SELECT geom FROM ... without ST_AsText, the output is hex-encoded EWKB — a long string like 0101000020E61000003D0AD7A3704D3E40.... This string is not meant for human reading and is not WKT — it's hex-encoded WKB. PostGIS clients (psql, psycopg2, pgAdmin) recognize the prefix and sometimes auto-decode it, adding to the confusion.

Practical rule: for debugging, migrations, and CSV-ETL pipelines — use WKT. For production storage and fast parsing — use WKB. PostGIS internally stores WKB, but speaks WKT through ST_AsText / ST_GeomFromText.

Where WKT appears in practice

PostGIS and MySQL Spatial. Every example in the PostGIS docs uses WKT: INSERT INTO places (name, geom) VALUES ('Brest', ST_GeomFromText('POINT(23.69 52.10)', 4326)). When you pg_dump --column-inserts to migrate to another PostGIS — that's WKT too. MySQL 8.0+ works identically: ST_GeomFromText('LINESTRING(...)').

CSV import into PostGIS / DuckDB. The most common way to load geodata from a spreadsheet — a CSV where one column contains WKT. PostGIS reads such a CSV via COPY places (name, geom) FROM 'places.csv' WITH (FORMAT csv) — if the geom column is declared as geometry, the engine auto-parses WKT. DuckDB Spatial does the same via SELECT ST_GeomFromText(wkt_column) FROM read_csv('places.csv'). More on this pipeline — in our CSV with geometry review.

Shapely and Python data science. Shapely (same author Sean Gillies as GeoJSON) reads and writes WKT via from shapely import wkt; geom = wkt.loads('POINT(30.32 59.93)') and wkt.dumps(geom). GeoPandas inherits this through the geometry column. When you need to hand spatial data to a colleague without a GIS environment — exporting to CSV with a WKT column is a universal solution.

Debug and logs. WKT is the only human-readable way to put a geometry in a log. logger.info(f"Failed to render geometry: {geom.wkt}") gives a string you can paste into https://wkt.maptiler.com/ or QGIS DB Manager and immediately see what went wrong. With WKB or binary GeoJSON that wouldn't work.

ETL pipelines via Airflow / dbt. In data warehouse stacks spatial data usually flows through WKT, because dbt models work on SQL and SQL understands WKT natively through ST_GeomFromText. Snowflake, BigQuery, Redshift Spectrum — all added WKT support to their geo functions in 2020-2022.

Map generation of Krasnodar city center

Strengths and weaknesses

Strengths. Human-readable — open in a text editor, see coordinates, understand the file. Universal support — every spatial DB in 2026 reads WKT natively. Simple parser — the ST_GeomFromText implementation fits in 500 lines of code (a GML parser, by comparison, is several thousand). OGC + ISO — official international standard. Extensibility through EWKT — the SRID prefix works de facto everywhere. Perfect for CSV pipelines — one column solves the problem of storing geometry in tabular form. Pre-RFC backward compat — 1999 WKT reads today.

Weaknesses. Size — text is 3–5x heavier than WKB. Parsing is 5–20x slower than WKB — lexical scanning of parentheses and commas doesn't scale to millions of features. Not a standalone format — there's no "WKT file"; usually it's a field in CSV / SQL / JSON. No attributes — only geometry, the rest must live in adjacent columns. No topology — lines and polygons don't know about shared boundaries. No CRS inside (without EWKT) — so POINT(30.32 59.93) without context could mean WGS 84 lat/lon, EPSG:3857 web-mercator, or coordinates in any local CRS. CRS has to be passed separately.

WKT vs GeoJSON vs Shapefile

These three formats often stand side by side in "how to save geodata" discussions, but they solve different problems:

  • WKT is a single-geometry serialization format, usually as a field in SQL / CSV / DataFrame. Attributes live in adjacent columns. Main use case — DB and ETL.
  • GeoJSON is a file format with a list of features, each with its own geometry plus an arbitrary JSON properties object. Main use case — web and inter-service exchange.
  • Shapefile is a set of binary files (.shp + .dbf + .shx + optionally .prj) with fixed attribute types and constraints (10 chars per field name, 2 GB per file, one geometry type per file). Main use case — Esri-compatible desktop GIS.

WKT is the lowest-level of the three: it's just a string. GeoJSON and Shapefile add file structure and an attribute container. That means WKT combines well with other formats — CSV with a WKT column, a JSON object with a WKT field, a SQL table with a geometry column. But WKT on its own as an "OSM area export" is not the most convenient solution because you need somewhere to put the tags (name, building, highway, etc.). So WKT exporters usually emit TSV or CSV, where the first column is tags as JSON or key=value, and the last column is the WKT geometry.

Workflow in OSM2CDR

Our wkt_exporter.py is ExporterType.LAYERS with the base class LayersExporter. Pipeline:

Step 1: Load layers from PostGIS. local_osm_loader.py via the WKB fast-path pulls vector data from the local OSM database. Returns Dict[layer_name, List[Feature]], where each Feature is a dict with tags and a WKB geometry.

Step 2: Convert WKB → WKT. Through Shapely or a direct GEOS GEOSGeomToWKT() call. Optionally with precision tuning — default 7 decimal places, which is around 1 cm at the equator for web precision.

Step 3: Serialize as TSV. Each feature on its own line: osm_id<TAB>layer<TAB>tags_json<TAB>wkt. Header: osm_id\tlayer\ttags\twkt\n. UTF-8 encoding for tags (Cyrillic and CJK characters in name:*), pure ASCII for WKT.

Step 4: Optional EWKT mode. If the user picked a CRS other than WGS 84 — the exporter reprojects geometries via PostGIS ST_Transform at step 1, and prepends SRID=NNNN; to the WKT.

Step 5: Streaming write. For bboxes with millions of features the exporter doesn't load everything in memory — it writes each feature directly to the output file via csv.writer with QUOTE_MINIMAL. Memory footprint — O(1).

The resulting file imports into PostGIS in one query:

CREATE TABLE osm_export (
    osm_id BIGINT, layer TEXT, tags JSONB, geom GEOMETRY
);
COPY osm_export FROM 'export.tsv' WITH (FORMAT csv, DELIMITER E'\t', HEADER true);
UPDATE osm_export SET geom = ST_SetSRID(ST_GeomFromText(wkt::text), 4326);

Or in Python via GeoPandas:

import geopandas as gpd
from shapely import wkt
import pandas as pd

df = pd.read_csv('export.tsv', sep='\t')
df['geometry'] = df['wkt'].apply(wkt.loads)
gdf = gpd.GeoDataFrame(df, geometry='geometry', crs='EPSG:4326')

Map generation of Rome city center

FAQ

How does WKT differ from GeoJSON? WKT is a representation of a single geometry without attributes, usually as a field in SQL / CSV. GeoJSON is a file format with a list of features, each with its own properties. WKT is shorter (no JSON wrapper) but requires an external container for attributes.

What is EWKT and how does it differ from WKT? EWKT is a PostGIS extension that adds a SRID prefix: SRID=4326;POINT(30.32 59.93). Not part of the OGC standard but supported by all major libraries (PostGIS, SpatiaLite, Shapely, GDAL). Used when CRS needs to travel with the geometry.

Can WKT store 3D geometries? Yes, via the Z variant: POINT Z (30.32 59.93 12.5). Added in OGC 06-103r4 (2010). There are also M (measure) and ZM (both). Supported by PostGIS 2.0+, SpatiaLite, Shapely 1.7+, GDAL 2.0+.

Is WKT 2 a new version for geometries? No, it's a completely different standard. WKT 2 (ISO 19162:2019) is a text format for coordinate reference systems (CRS), replacing old PROJ.4 strings and Esri .prj files. It has no relation to POINT(...) and POLYGON(...). Used in PROJ 6+, GDAL 3+, PostGIS 3+ for describing CRS in spatial_ref_sys.srtext.

Why does PostGIS return a hex string instead of WKT? SELECT geom FROM ... without a wrapper returns the internal representation — hex-encoded EWKB (0101000020E6100000...). For readable WKT you need SELECT ST_AsText(geom) or SELECT ST_AsEWKT(geom). This is a common beginner mistake — confusing hex-EWKB with WKT.

Sources

← All articles