Skip to main content

encode_geojson

The encode_geojson template is a geospatial analyzer: instead of breaking text into word tokens, it reads a geometry and emits the S2 cell-ID terms that cover it. Those terms are what the inverted index stores and matches, so a JSON or GEOMETRY column indexed through encode_geojson can be queried with spatial predicates such as containment, intersection and distance.

As a function: encode_geojson(value, type := 'shape', coding := 'source', max_cells := 20, min_level := 4, max_level := 23, level_mod := 1, optimize_for_space := false) — the value first, then the options in the order below. See tokenizer functions for how a value, a list and a chain of calls behave.

Query
SELECT array_length(encode_geojson('{"type": "Point", "coordinates": [2.5, 1.5]}'::JSON), 1) AS cells;
Result
 cells-------    20

How it works

Geometries are supplied as GeoJSON in a JSON column, or as WKB in a GEOMETRY column. From JSON the analyzer reads a geometry object whose type is Point, LineString, Polygon, MultiPoint, MultiLineString or MultiPolygon, matched case-insensitively; GeometryCollection is rejected. A bare coordinate array such as [13.405, 52.52] is also accepted and read as a point, in GeoJSON's [longitude, latitude] order. The analyzer approximates each shape with a covering of S2 cells at a range of levels and emits the cell IDs as terms; a query shape is covered the same way, and rows match when their coverings overlap.

CODING controls how a representative geometry is stored alongside the index terms so predicates can be evaluated precisely. It leaves the emitted terms alone, except under s2latlngu32: that coding snaps polygon vertices onto its 32-bit grid before the covering is computed, so a polygon's covering can differ.

  • source (the default) writes no derived encoding: the original column value is stored in the index and re-parsed at query time, so predicates see the exact input geometry.
  • s2point, s2latlngf64 and s2latlngu32 write a compact S2 encoding into a synthetic index column instead — of the whole geometry, or of just its centroid under TYPE = centroid. They are progressively smaller: an S2 unit vector as three doubles, a latitude/longitude pair as two doubles, then that pair with each coordinate quantized to 32 bits. Only s2point is lossless; both LatLng codings go through latitude and longitude, and s2latlngu32 is the coarsest of the three. A GEOMETRY column accepts source and s2point; the two LatLng codings are refused for it.

TYPE controls what each geometry is reduced to before terms are computed: shape (the default) indexes the whole geometry, centroid indexes only its centroid point whatever the input geometry, and point accepts point inputs only — any other geometry produces no terms. Under shape, an input that parses to a single point takes the point path below, and a shape that does not contain its own centroid has the centroid's levels appended after its covering terms.

Terms are BLOBs in two forms. An ancestor term is exactly 8 bytes, the big-endian S2 cell ID. A covering term is 9 bytes: a $ marker byte followed by the same 8 bytes. Only TYPE = shape over a non-point geometry produces covering terms, and the geometry itself is never emitted as a term.

A point — TYPE = point, TYPE = centroid, or a Point geometry under TYPE = shape — expands to one ancestor term per S2 level, walking from MIN_LEVEL to MAX_LEVEL in steps of LEVEL_MOD, coarsest first. That is floor((MAX_LEVEL - MIN_LEVEL) / LEVEL_MOD) + 1 terms — 20 with the defaults. A covering emits, in S2 cell order for each of its cells: a covering term while the cell sits below the effective finest level (MAX_LEVEL minus (MAX_LEVEL - MIN_LEVEL) mod LEVEL_MOD); an ancestor term for the cell itself when it is at that effective level, and unconditionally unless OPTIMIZE_FOR_SPACE is set; then ancestor terms for its ancestors, stepping down by LEVEL_MOD to MIN_LEVEL and stopping as soon as the previous covering cell already covered them. Terms are not deduplicated, so a cell can repeat where an appended centroid chain meets ancestors already emitted.

Geo dictionaries support no feature flagsFREQUENCY, POSITION, NORM and OFFSET are all rejected at CREATE TEXT SEARCH DICTIONARY — and no text options such as CASE or LOCALE apply.

A geometry that does not parse simply produces no terms: invalid JSON, a missing or non-array coordinates, an unrecognized type or GeometryCollection, an invalid LineString or Polygon, a non-point geometry under TYPE = point, and a degenerate geometry whose centroid is not a unit vector, such as a zero-area polygon. At index time such a row is indexed without geo terms and no error is raised.

When to use encode_geojson vs encode_geopoint

Use encode_geojson when rows hold arbitrary geometries — polygons, lines, multi-geometries — points already expressed as GeoJSON, or a GEOMETRY column. Reach for encode_geopoint instead when every row is a single point whose latitude and longitude live in two separate fields of a JSON object; it builds the point directly without GeoJSON assembly. Given the same level options both templates emit the same terms for a point, so a point indexed either way is queried identically.

Options

OptionTypeDefaultDescription
TYPEstring'shape'What each geometry is reduced to: shape, centroid or point
CODINGstring'source'How a representative geometry is stored: source, s2point, s2latlngf64, s2latlngu32
MIN_LEVELinteger4Coarsest S2 cell level indexed (0–30); must be ≤ MAX_LEVEL
MAX_LEVELinteger23Finest S2 cell level indexed (0–30); ~1 m precision at level 23
MAX_CELLSinteger20Size target for the S2 covering (0–2147483647); only affects TYPE = shape over a non-point geometry
LEVEL_MODinteger1Level step between the emitted cells, counted up from MIN_LEVEL (1, 2 or 3)
OPTIMIZE_FOR_SPACEbooleanfalseOptimize the S2 covering for space rather than speed; only affects TYPE = shape over a non-point geometry

TYPE and CODING values are matched case-insensitively; a value outside the list fails with invalid value in "type" parameter or invalid value in "coding" parameter. MAX_CELLS is a target rather than a hard cap: it bounds how much work the coverer does, and a covering often uses fewer cells. MIN_LEVEL takes priority over it, so a geometry that is large relative to MIN_LEVEL can produce more. Levels outside their range are reported by option name — geo_json: 'min_level' out of bounds: [0..30]., geo_json: 'level_mod' out of bounds: [1..3]., geo_json: 'min_level' should be less than or equal to 'max_level'.

Usage

Create the dictionary, then attach it to a JSON or GEOMETRY column in a USING inverted index. A plain VARCHAR column is rejected for geo analyzers, and a GEOMETRY column must declare the CRS84 coordinate reference system, which is spelled OGC:CRS84.

Query
CREATE TEXT SEARCH DICTIONARY geojson_shape AS    encode_geojson();

With CODING = 's2point' the same geometries are stored as compact S2 points — the compact choice for a GEOMETRY column, which accepts only source and s2point:

Query
CREATE TEXT SEARCH DICTIONARY geojson_s2 AS    encode_geojson(coding := 's2point');

ts_lexize shows the cell-ID terms a geometry expands into. It always takes GeoJSON text, even for a dictionary attached to a GEOMETRY column. The terms are BLOBs, so the example projects them through hex(), and the dictionary name has to be a constant — ts_lexize refuses a non-constant name for a dictionary that produces BLOB terms. Here a single point in central Berlin gives one ancestor cell per level, coarsest first:

Query
SELECT hex(cell) AS cellFROM unnest(ts_lexize(    'geojson_s2',    '{"type":"Point","coordinates":[13.405,52.52]}')) AS cell;
Result
 cell------------------ 47B0000000000000 47AC000000000000 47A9000000000000 47A8400000000000 47A8500000000000 47A8540000000000 47A8510000000000 47A851C000000000 47A851D000000000 47A851DC00000000 47A851DF00000000 47A851DFC0000000 47A851DFF0000000 47A851DFEC000000 47A851DFED000000 47A851DFECC00000 47A851DFECD00000 47A851DFECCC0000 47A851DFECC90000 47A851DFECC9C000

For the full indexing-and-query walkthrough — ST_Intersects, ST_Contains and distance predicates over both JSON and GEOMETRY columns — see Geospatial Search.

See also