RFC 99: Geometry coordinate precision


Even Rouault


even.rouault @ spatialys.com




Adopted, implemented in GDAL 3.9


GDAL 3.9


This RFC aims at introducing optional metadata to specify the coordinate precision of geometries, to be able to round appropriately coordinates and limit the number of decimals when exporting to text-based formats or nullify least-significant bits for binary formats. That metadata will be stored into and read from formats that can support it.


The aim is multiple:

  • reducing file size. For text-based formats, rounding and truncating to the specified precision directly reduce file size. For binary formats, using that information to zero least-significant bits can increase the potential when applying afterwards lossless compression methods (typically zipping a file).

  • presenting the user with hints on the precision of the data he/she accesses. This can be used by user interfaces build on top of GDAL to display geometry coordinates with an appropriate number of decimals.

  • a few drivers (GeoJSON, JSONFG, OpenFileGDB) have layer creation options to specify coordinate precision, but there is currently no driver agnostic way of specifying it.

For example, currently, when exporting a file to GML, 15 significant decimal digits (ie the total of digits for the integral and decimal parts) are used, which corresponds to a 0.1 micrometer precision for geography coordinates. The same holds for regular GeoJSON export, unless the RFC 7946 variant is selected, in which case only 7 decimal digits after decimal separators are used. However this is a layer creation option, which means that it is no longer remembered when data is edited/appended to an existing layer (see https://github.com/qgis/QGIS/issues/56335).

For binary formats using IEEE-754 double-precision encoding of real numbers, one can show that at least the last 16 least-significants bits (ie the last 2 bytes of 8) of a coordinate can be set to zero while keeping a 1 mm precision (which corresponds to about 8.9e-9 degree). On a test dataset, setting a 1 mm precision reduced the size of the .zip of the .gpkg file from 766 MB to 667 MB (13% size decrease). If only a 1 meter precision is wished, this increases to 26 useless least-significant bits.

>>> import math
>>> earth_radius_in_meter = 6378137
>>> one_degree_in_meter = earth_radius_in_meter * math.pi / 180.0
>>> mm_prec_in_degree = 1e-3 / one_degree_in_meter
>>> print(mm_prec_in_degree)
>>> max_integer_part = 180  # for coordinates in range [-180,180]
>>> significant_bits_needed = math.ceil(math.log2(max_integer_part)) + math.ceil(math.log2(1 / mm_prec_in_degree)) + 1
>>> mantissa_bit_size = 52
>>> unused_bits = mantissa_bit_size - significant_bits_needed
>>> print(unused_bits)

C and C++ API extensions and changes

OGRGeomCoordinatePrecision class

A new OGRGeomCoordinatePrecision class is introduced in the ogr_geomcoordinateprecision.h file:

/** Geometry coordinate precision.
 * This may affect how many decimal digits (for text-based output) or bits
 * (for binary encodings) are used to encode geometries.
 * It is important to note that the coordinate precision has no direct
 * relationship with the "physical" accuracy. It is generally advised that
 * the resolution (precision) be at least 10 times smaller than the accuracy.
struct CPL_DLL OGRGeomCoordinatePrecision
    static constexpr double UNKNOWN = 0;

    /** Resolution for the coordinate precision of the X and Y coordinates.
     * Expressed in the units of the X and Y axis of the SRS.
     * For example for a projected SRS with X,Y axis unit in meter, a value
     * of 1e-3 corresponds to a 1 mm precision.
     * For a geographic SRS (on Earth) with axis unit in degree, a value
     * of 8.9e-9 (degree) also corresponds to a 1 mm precision.
     * Set to 0 if unknown.
    double dfXYResolution = UNKNOWN;

    /** Resolution for the coordinate precision of the Z coordinate.
     * Expressed in the units of the Z axis of the SRS.
     * Set to 0 if unknown.
    double dfZResolution = UNKNOWN;

    /** Resolution for the coordinate precision of the M coordinate.
     * Set to 0 if unknown.
    double dfMResolution = UNKNOWN;

    /** Map from a format name to a list of format specific options.
     * This can be for example used to store FileGeodatabase
     * xytolerance, xorigin, yorigin, etc. coordinate precision grids
     * options, which can be help to maximize preservation of coordinates in
     * FileGDB -> FileGDB conversion processes.
    std::map<std::string, CPLStringList> oFormatSpecificOptions{};

     * \brief Set the resolution of the geometry coordinate components.
     * For the X, Y and Z ordinates, the precision should be expressed in meter,
     * e.g 1e-3 for millimetric precision.
     * Resolution should be stricty positive, or set to
     * OGRGeomCoordinatePrecision::UNKNOWN when unknown.
     * @param poSRS Spatial reference system, used for metric to SRS unit conversion
     *              (must not be null)
     * @param dfXYMeterResolution Resolution for for X and Y coordinates, in meter.
     * @param dfZMeterResolution Resolution for for Z coordinates, in meter.
     * @param dfMResolutionIn Resolution for for M coordinates.
    void SetFromMeter(const OGRSpatialReference *poSRS,
                      double dfXYMeterResolution,
                      double dfZMeterResolution, double dfMResolution);

     * \brief Return equivalent coordinate precision setting taking into account
     * a change of SRS.
     * @param poSRSSrc Spatial reference system of the current instance
     *                 (if null, meter unit is assumed)
     * @param poSRSDst Spatial reference system of the returned instance
     *                 (if null, meter unit is assumed)
     * @return a new OGRGeomCoordinatePrecision instance, with a poSRSDst SRS.
    ConvertToOtherSRS(const OGRSpatialReference *poSRSSrc,
                      const OGRSpatialReference *poSRSDst) const;

     * \brief Return the number of decimal digits after the decimal point to
     * get the specified resolution.
    static int ResolutionToPrecision(double dfResolution);

Corresponding additions at the C API level:

/** Value for a unknown coordinate precision. */

/** Opaque type for OGRGeomCoordinatePrecision */
typedef struct OGRGeomCoordinatePrecision *OGRGeomCoordinatePrecisionH;

OGRGeomCoordinatePrecisionH CPL_DLL OGRGeomCoordinatePrecisionCreate(void);
void CPL_DLL OGRGeomCoordinatePrecisionDestroy(OGRGeomCoordinatePrecisionH);
double CPL_DLL
double CPL_DLL
double CPL_DLL
char CPL_DLL **
CSLConstList CPL_DLL OGRGeomCoordinatePrecisionGetFormatSpecificOptions(
    OGRGeomCoordinatePrecisionH, const char *pszFormatName);
void CPL_DLL OGRGeomCoordinatePrecisionSet(OGRGeomCoordinatePrecisionH,
                                           double dfXYResolution,
                                           double dfZResolution,
                                           double dfMResolution);
void CPL_DLL OGRGeomCoordinatePrecisionSetFromMeter(OGRGeomCoordinatePrecisionH,
                                                    OGRSpatialReferenceH hSRS,
                                                    double dfXYMeterResolution,
                                                    double dfZMeterResolution,
                                                    double dfMResolution);
void CPL_DLL OGRGeomCoordinatePrecisionSetFormatSpecificOptions(
    OGRGeomCoordinatePrecisionH, const char *pszFormatName,
    CSLConstList papszOptions);

OGRGeomFieldDefn class

The existing OGRGeomFieldDefn is extended with a new OGRGeomCoordinatePrecision member, and associated getter and setter methods.

class OGRGeomFieldDefn
        const OGRGeomCoordinatePrecision& GetCoordinatePrecision() const;

        void SetCoordinatePrecision(const OGRGeomCoordinatePrecision& prec);

        OGRGeomCoordinatePrecision m_oCoordPrecision{};

New corresponding C API:

    CPL_DLL OGR_GFld_GetCoordinatePrecision(OGRGeomFieldDefnH);
void CPL_DLL OGR_GFld_SetCoordinatePrecision(OGRGeomFieldDefnH,

OGRGeometry class

The existing OGRGeometry is extended with the following new SetPrecision method which is a wrapper of the GEOSGeom_setPrecision_r function:

/** Set the geometry's precision, rounding all its coordinates to the precision
 * grid, and making sure the geometry is still valid.
 * This is a stronger version of roundCoordinates().
 * Note that at time of writing GEOS does no supported curve geometries. So
 * currently if this function is called on such a geometry, OGR will first call
 * getLinearGeometry() on the input and getCurveGeometry() on the output, but
 * that it is unlikely to yield to the expected result.
 * @param dfGridSize size of the precision grid, or 0 for FLOATING
 *                 precision.
 * @param nFlags The bitwise OR of zero, one or several of OGR_GEOS_PREC_NO_TOPO
 *               and OGR_GEOS_PREC_KEEP_COLLAPSED
 * @return a new geometry or NULL if an error occurs.

OGRGeometry *OGRGeometry::SetPrecision(double dfGridSize, int nFlags) const;

New corresponding C API:

/** This option causes OGR_G_SetPrecision()
  * to not attempt at preserving the topology */
#define OGR_GEOS_PREC_NO_TOPO (1 << 0)

/** This option causes OGR_G_SetPrecision()
  * to retain collapsed elements */

OGRGeometryH OGR_G_SetPrecision(OGRGeometryH, double dfGridSize, int nFlags);

Note that this method is not automatically run by the writing side of drivers, which assume that the passed geometries are valid once rounded with the specified coordinate precision metadata.

However it is invoked when the -xyRes switch of ogr2ogr is passed.

It may also be triggered by setting the new OGR_APPLY_GEOM_SET_PRECISION configuration option to YES, for geometries passed to OGRLayer::CreateFeature() and OGRLayer::SetFeature() before they are passed to the driver.

A closely related roundCoordinates method is also introduced:

/** Round coordinates of the geometry to the specified precision.
 * Note that this is not the same as OGRGeometry::SetPrecision(). The later
 * will return valid geometries, whereas roundCoordinates() does not make
 * such guarantee and may return geometries with invalidities, if they are
 * not compatible of the specified precision. roundCoordinates() supports
 * curve geometries, whereas SetPrecision() does not currently.
 * One use case for roundCoordinates() is to undo the effect of
 * quantizeCoordinates().
 * @param sPrecision Contains the precision requirements.
 * @since GDAL 3.9
 void roundCoordinates(const OGRGeomCoordinatePrecision &sPrecision);

WKB export

WKB export methods will be modified in a similar way as in the prototype https://github.com/OSGeo/gdal/pull/6974 to nullify least significant bits from the precision specifications.

More specifically the following 2 new classes are added:

/** Geometry coordinate precision for a binary representation.
struct CPL_DLL OGRGeomCoordinateBinaryPrecision
    int nXYBitPrecision =
        INT_MIN; /**< Number of bits needed to achieved XY precision. Typically
                    computed with SetFromResolution() */
    int nZBitPrecision =
        INT_MIN; /**< Number of bits needed to achieved Z precision. Typically
                    computed with SetFromResolution() */
    int nMBitPrecision =
        INT_MIN; /**< Number of bits needed to achieved M precision. Typically
                    computed with SetFromResolution() */

    void SetFrom(const OGRGeomCoordinatePrecision &);

/** WKB export options.
struct CPL_DLL OGRwkbExportOptions
    OGRwkbByteOrder eByteOrder = wkbNDR;           /**< Byte order */
    OGRwkbVariant eWkbVariant = wkbVariantOldOgc;  /**< WKB variant. */
    OGRGeomCoordinateBinaryPrecision sPrecision{}; /**< Binary precision. */

And the C++ OGRGeometry exportToWkb virtual method is modified to have the following prototype:

virtual OGRErr exportToWkb(unsigned char *,
                           const OGRwkbExportOptions * = nullptr) const = 0;

The existing method with signature OGRErr exportToWkb(OGRwkbByteOrder, unsigned char *, OGRwkbVariant = wkbVariantOldOgc) const is kept and call the new virtual method.

New corresponding C API:

/** Opaque type for WKB export options */
typedef struct OGRwkbExportOptions OGRwkbExportOptions;

OGRwkbExportOptions CPL_DLL *OGRwkbExportOptionsCreate(void);
void CPL_DLL OGRwkbExportOptionsDestroy(OGRwkbExportOptions *);
void CPL_DLL OGRwkbExportOptionsSetByteOrder(OGRwkbExportOptions *,
void CPL_DLL OGRwkbExportOptionsSetVariant(OGRwkbExportOptions *,
void CPL_DLL OGRwkbExportOptionsSetPrecision(OGRwkbExportOptions *,
OGRErr CPL_DLL OGR_G_ExportToWkbEx(OGRGeometryH, unsigned char *,
                                   const OGRwkbExportOptions *);

OGRLayer CreateLayer()/ICreateLayer() changes

The signature of the current OGRLayer::ICreateLayer() protected method (implemented by drivers) will be changed from

virtual OGRLayer *ICreateLayer(
        const char *pszName, const OGRSpatialReference *poSpatialRef = nullptr,
        OGRwkbGeometryType eGType = wkbUnknown, char **papszOptions = nullptr);


virtual OGRLayer *ICreateLayer(
        const char *pszName,
        const OGRGeomFieldDefn* poGeomFieldDefn = nullptr,
        CSLConstList papszOptions = nullptr);

This will require changes to out-of-tree drivers that implement it.

A corresponding non-virtual public method will also be added:

OGRLayer *CreateLayer(
        const char *pszName,
        const OGRGeomFieldDefn* poGeomFieldDefn,
        CSLConstList papszOptions = nullptr);

And the current CreateLayer() signature will be adapted to call the modified ICreateLayer().

And for the C API:

OGRLayerH CPL_DLL GDALDatasetCreateLayerFromGeomFieldDefn(
                                           GDALDatasetH, const char *,
                                           OGRGeomFieldDefnH hGeomFieldDefn,

A new GDAL_DCAP_HONOR_GEOM_COORDINATE_PRECISION driver capability will be added to advertise that a driver honours OGRGeomFieldDefn::GetCoordinatePrecision() when writing geometries. This may be useul for user interfaces that could offer an option to the user to specify the coordinate precision. Note however that the driver may not be able to store that precision in the dataset metadata.

There will be no provision to modify the coordinate precision of an existing layer geometry field with OGRLayer::AlterFieldDefn().

Driver changes

The following drivers will be modified to honour GDAL_DCAP_HONOR_GEOM_COORDINATE_PRECISION


The driver will compute the number of decimal digits after the decimal point to write as ceil(1. / log10(resolution))

The driver will be able to store and retrieve the coordinate precision metadata in the files it generates, by adding xy_coordinate_resolution and z_coordinate_resolution members at the FeatureCollection level.

The existing COORDINATE_PRECISION layer creation option, if specified, will take precedence over the settings coming from OGRGeomFieldDefn::GetCoordinatePrecision().


The driver will compute the number of decimal digits after the decimal point to write as ceil(1. / log10(resolution))

It will not be able to store it in its metadata.


Similar to GeoJSON. One subtelty is that this driver may write both the "place" geometry (generally in a non-WGS84 CRS) and the GeoJSON RFC7946 WGS84 "geometry".

The OGRGeomFieldDefn::GetCoordinatePrecision() will qualify the "place" geometry. The coordinate precision of the WGS84 "geometry" will be derived from the one of the "place" geometry with appropriate geographic/projected CRS and axis unit changes.

The coordinate precision metadata of the "place" member will be stored in xy_coordinate_resolution_place and z_coordinate_resolution_place members at the FeatureCollection level.

For the "geometry" member, the same xy_coordinate_resolution and z_coordinate_resolution members as the GeoJSON driver will be used.

The existing COORDINATE_PRECISION_PLACE or COORDINATE_PRECISION_GEOMETRY layer creation option, if specified, will take precedence over the settings coming from OGRGeomFieldDefn::GetCoordinatePrecision().


The driver will compute the number of decimal digits after the decimal point to write as ceil(1. / log10(resolution))

The driver will be able to store the coordinate precision metadata in the XML schema it generates by adding a xs:annotation/xs:appinfo element in the declaration of the geometry property, and with ogr:xy_coordinate_resolution and ogr:z_coordinate_resolution sub-elements. This should hopefully be ignored by readers that don't recognize that metadata (this will be the case of GDAL < 3.9)

<xs:element name="wkb_geometry" type="gml:SurfacePropertyType" nillable="true" minOccurs="0" maxOccurs="1">
      <xs:appinfo source="http://ogr.maptools.org/">


The driver will compute the number of decimal digits after the decimal point to write as ceil(1. / log10(resolution))

It will not be able to store it in its metadata. The possibility of storing the coordinate metadata in the .csvt side-car file has been considered, but it would not be backwards-compatible.


The GeoPackage driver will support reading and writing the geometry coordinate precision. By default, the geometry coordinate precision will only noted in metadata, and does not cause geometries that are written to be modified to comply with this precision.

Several settings may be combined to apply further processing:

  • the OGR_APPLY_GEOM_SET_PRECISION configuration option as described previously.

  • if the new DISCARD_COORD_LSB layer creation option is set to YES, the less-significant bits of the WKB geometry encoding which are not relevant for the requested precision are set to zero. This can improve further lossless compression stages, for example when putting a GeoPackage in an archive. Note however that when reading back such geometries and displaying them to the maximum precision, they will not "exactly" match the original OGRGeomCoordinatePrecision settings. However, they will round back to it. The value of the DISCARD_COORD_LSB layer creation option is written in the dataset metadata, and will be re-used for later edition sessions.

  • if the new UNDO_DISCARD_COORD_LSB_ON_READING layer creation option is set to YES (only makes sense if the DISCARD_COORD_LSB layer creation option is also set to YES), when reading back geometries from a dataset, the OGRGeometry::roundCoordinates method will be applied so that the geometry coordinates exactly match the original specified coordinate precision. That option will only be honored by GDAL 3.9 or later.

Implementation details: the coordinate precision is stored in a record in each of the gpkg_metadata and gpkg_metadata_reference table, with the following additional constraints on top of the ones imposed by the GeoPackage specification:

  • gpkg_metadata.md_standard_uri = 'http://gdal.org'

  • gpkg_metadata.mime_type = 'text/xml'

  • gpkg_metadata.metadata = '<CoordinatePrecision xy_resolution="{xy_resolution}" z_resolution="{z_resolution}" m_resolution="{m_resolution}" discard_coord_lsb={true or false} undo_discard_coord_lsb_on_reading={true or false} />'

  • gpkg_metadata_reference.reference_scope = 'column'

  • gpkg_metadata_reference.table_name = '{table_name}'

  • gpkg_metadata_reference.column_name = '{geometry_column_name}'

Note that the xy_resolution, z_resolution or m_resolution attributes of the XML CoordinatePrecision element are optional. Their numeric value is expressed in the units of the SRS for xy_resolution and z_resolution.

INSERT INTO gpkg_metadata VALUES(1,'dataset','http://gdal.org','text/xml',
    '<CoordinatePrecision xy_resolution="8.9e-9" z_resolution="1e-3" m_resolution="1e-3" discard_coord_lsb="false" undo_discard_coord_lsb_on_reading="false"></CoordinatePrecision>');
INSERT INTO gpkg_metadata_reference VALUES('column','poly','geom',NULL,'2023-10-22T21:13:43.282Z',1,NULL);


OGRGeomCoordinatePrecision::dfXYResolution (resp. dfZResolution, dfMResolution) directly map to 1. / xyscale (resp. 1 / zscale, 1 / mscale) in the declaration of the coordinate grid precision options of the FileGeodatabase format (cf https://help.arcgis.com/en/sdk/10.0/java_ao_adf/conceptualhelp/engine/index.html#//00010000037m000000).

Consequently the OpenFileGDB driver can be modified in reading and writing to fully honour OGRGeomCoordinatePrecision.

The driver will also get and set other coordinate grid precision options, such as the origin and tolerance, values in the FileGeodatabase key of the OGRGeomCoordinatePrecision::oFormatSpecificOptions member.

The existing XYSCALE, ZSCALE and MSCALE layer creation options, if specified, will take precedence over the settings coming from OGRGeomFieldDefn::GetCoordinatePrecision().


Modified to have exactly the same behavior as OpenFileGDB.


The driver will read the geometry coordinate precision from the source geometry field, or possibly overridden with the following elements in the XML VRT:




ogrinfo will be modified to honour OGRGeomCoordinatePrecision when outputting WKT geometries (or GeoJSON geometries for the -json output)


ogr2ogr will forward by default the OGRGeomCoordinatePrecision of the input layer to the output layer, but of course it will only have effects for drivers honouring GDAL_DCAP_HONOR_GEOM_COORDINATE_PRECISION.

When reprojection occurs, the coordinate precision will be adjusted to take into account geographic vs projected CRS changes and unit changes.

The following options will be added:

  • -xyRes <val>: XY coordinate resolution. Nominally in the unit of the X and Y SRS axis. Appending a m, mm or deg suffix will be also supported. A warning will be emitted if the user specifies this option when creating a new layer for a driver that does not advertise GDAL_DCAP_HONOR_GEOM_COORDINATE_PRECISION.

  • -zRes <val>: Z coordinate resolution. Nominally in the unit of the Z SRS axis. Appending a m or mm suffix will be also supported.

  • -mRes <val>: M coordinate resolution.

  • -unsetCoordPrecision: to disable automatic propagation of the input coordinate precision to the output.

Out of scope

While there is an obvious logical connection with GEOS' PrecisionModel (https://libgeos.org/doxygen/classgeos_1_1geom_1_1PrecisionModel.html), this RFC does not tie the introduced OGR coordinate precision metadata with it. Tying both would require either adding a reference to a OGRGeomCoordinatePrecision as a member of the OGRGeometry class (which would have some extra RAM usage implications), or as a parameter in OGRGeometry GEOS related methods.

Quantization of raster pixel values (e.g. the DISCARD_LSB creation option of the GeoTIFF driver) is also slightly connected.

SWIG bindings

The new C functions are bound to SWIG.

class ogr.GeomCoordinatePrecision:

  void Set(double xyResolution, double zResolution, double mResolution);
  void SetFromMeter(osr.SpatialReference srs, double xyMeterResolution, double zMeterResolution, double mResolution);
  double GetXYResolution();
  double GetZResolution();
  double GetMResolution();
  char **GetFormats();  // as a list
  char ** GetFormatSpecificOptions(const char* formatName); // as a dictionary
  void SetFormatSpecificOptions(const char* formatName, char **formatSpecificOptions) // formatSpecificOptions as a dictionary

ogr.GeomCoordinatePrecision ogr.CreateGeomCoordinatePrecision();

class ogr.GeomFieldDefn:
    ogr.GeomCoordinatePrecision GetCoordinatePrecision();
    void SetCoordinatePrecision(ogr.GeomCoordinatePrecision coordPrec);

class gdal.Dataset:
    Layer CreateLayerFromGeomFieldDefn(const char* name, ogr.GeomFieldDefn geom_field, char** options=0);


Tests will be added for the new API and the modified drivers.

Backward compatibility

The C and C++ API are extended.

The change of the ICreateLayer() virtual method is an ABI change, and will require source code changes to out-of-tree drivers implementing it.

MIGRATION_GUIDE.TXT will mention that and point to this RFC.

Design discussion

This paragraph discusses a number of thoughts that arose during the writing of this RFC but were not kept.

While changing ICreateLayer() prototype, which requires the tedious process of changing it in more than 50 drivers, I've also considered introducing an additional OGRLayerCreationContext argument, but I've decided against if, as it is unclear if it would be that useful. For example, in most ogr2ogr scenarios, the final extent and feature count is unknown at the start of the process.

struct OGRLayerCreationContext
    OGRExtent3D sExtent;
    int64_t     nFeatureCount;

OGRLayer *ICreateLayer(
        const char *pszName, const OGRGeomFieldDefn* poFieldDefn = nullptr,
        const OGRLayerCreationContext& sContext = OGRLayerCreationContext(),
        CSLConstList papszOptions = nullptr);

Voting history

+1 from PSC members EvenR and HowardB. +0 from KurtS