Task 499: .OV2 File Format

Task 499: .OV2 File Format

.OV2 File Format Specifications

The .OV2 file format is a binary format used by TomTom GPS devices to store Points of Interest (POI). It has no file header or magic number; it is simply a concatenation of variable-length records. All integers are signed 32-bit values stored in little-endian byte order. Longitude and latitude values are stored as integers representing degrees multiplied by 100,000 (e.g., -118.5 degrees is -11,850,000).

There are four record types:

  • Type 0 (Deleted POI Record): Marks a deleted POI. Structure: type (1 byte = 0), record length (4 bytes), longitude (4 bytes), latitude (4 bytes), null-terminated string (optional, often empty).
  • Type 1 (Skipper Record): Used for optimization to skip irrelevant sections. Fixed size of 21 bytes. Structure: type (1 byte = 1), block length (4 bytes, bytes to skip if outside bounding box), NE longitude (4 bytes), NE latitude (4 bytes), SW longitude (4 bytes), SW latitude (4 bytes).
  • Type 2 (Regular POI Record): Standard POI. Structure: type (1 byte = 2), record length (4 bytes), longitude (4 bytes), latitude (4 bytes), null-terminated string (name).
  • Type 3 (Extended POI Record): Extended POI with additional data. Structure: type (1 byte = 3), record length (4 bytes), longitude (4 bytes), latitude (4 bytes), one or more null-terminated strings (name and extra data).

The file is read by parsing records sequentially until the end.

  1. List of all properties of this file format intrinsic to its file system:
  • Binary format with little-endian signed 32-bit integers.
  • No file header or signature.
  • Sequence of variable-length records without padding.
  • Record type (1 byte, values 0-3).
  • Record length (4 bytes, for types 0, 2, 3; includes type and length fields).
  • Longitude (4 bytes, signed int32, degrees * 100,000; for types 0, 2, 3 and bounding box in type 1).
  • Latitude (4 bytes, signed int32, degrees * 100,000; for types 0, 2, 3 and bounding box in type 1).
  • Null-terminated string(s) (variable length; name for type 2, name + extra for type 3, optional for type 0).
  • Block length (4 bytes, for type 1; bytes to skip).
  • Northeast longitude (4 bytes, for type 1).
  • Northeast latitude (4 bytes, for type 1).
  • Southwest longitude (4 bytes, for type 1).
  • Southwest latitude (4 bytes, for type 1).
  • File extension .OV2 associated with TomTom POI databases.
  • Typical file size depends on number of POIs; no fixed limit.
  1. Two direct download links for .OV2 files:
  1. Ghost blog embedded HTML JavaScript for drag and drop to dump properties:
OV2 File Parser
Drag and drop .OV2 file here
  1. Python class for .OV2:
import struct

class OV2Handler:
    def __init__(self, filepath=None):
        self.filepath = filepath
        self.records = []
        if filepath:
            self.read()

    def read(self):
        with open(self.filepath, 'rb') as f:
            data = f.read()
        offset = 0
        while offset < len(data):
            type_ = data[offset]
            offset += 1
            if type_ == 1:
                block_length, ne_lon, ne_lat, sw_lon, sw_lat = struct.unpack_from('<5i', data, offset)
                self.records.append({
                    'type': type_,
                    'block_length': block_length,
                    'ne_lon': ne_lon / 100000,
                    'ne_lat': ne_lat / 100000,
                    'sw_lon': sw_lon / 100000,
                    'sw_lat': sw_lat / 100000
                })
                offset += 20
            elif type_ in (0, 2, 3):
                length, lon, lat = struct.unpack_from('<3i', data, offset)
                offset += 12
                str_end = offset
                strings = []
                while str_end < offset + (length - 13):
                    str_start = str_end
                    while str_end < len(data) and data[str_end] != 0:
                        str_end += 1
                    strings.append(data[str_start:str_end].decode('utf-8'))
                    str_end += 1  # skip null
                self.records.append({
                    'type': type_,
                    'length': length,
                    'lon': lon / 100000,
                    'lat': lat / 100000,
                    'strings': strings
                })
                offset += length - 13
            else:
                break

    def print_properties(self):
        for record in self.records:
            print(record)
            print('---')

    def write(self, filepath=None):
        if not filepath:
            filepath = self.filepath or 'output.ov2'
        with open(filepath, 'wb') as f:
            for record in self.records:
                type_ = record['type']
                f.write(struct.pack('<B', type_))
                if type_ == 1:
                    f.write(struct.pack('<5i', record['block_length'], int(record['ne_lon'] * 100000), int(record['ne_lat'] * 100000), int(record['sw_lon'] * 100000), int(record['sw_lat'] * 100000)))
                elif type_ in (0, 2, 3):
                    strings_bytes = b''.join(s.encode('utf-8') + b'\0' for s in record['strings'])
                    length = 1 + 4 + 4 + 4 + len(strings_bytes)
                    f.write(struct.pack('<3i', length, int(record['lon'] * 100000), int(record['lat'] * 100000)))
                    f.write(strings_bytes)
  1. Java class for .OV2:
import java.io.*;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;

public class OV2Handler {
    private String filepath;
    private java.util.List<java.util.Map<String, Object>> records = new java.util.ArrayList<>();

    public OV2Handler(String filepath) throws IOException {
        this.filepath = filepath;
        if (filepath != null) {
            read();
        }
    }

    public void read() throws IOException {
        try (FileInputStream fis = new FileInputStream(filepath)) {
            byte[] data = fis.readAllBytes();
            ByteBuffer bb = ByteBuffer.wrap(data).order(ByteOrder.LITTLE_ENDIAN);
            int offset = 0;
            while (offset < data.length) {
                byte type = bb.get(offset);
                offset += 1;
                java.util.Map<String, Object> record = new java.util.HashMap<>();
                record.put("type", (int) type);
                if (type == 1) {
                    int blockLength = bb.getInt(offset);
                    offset += 4;
                    int neLon = bb.getInt(offset);
                    offset += 4;
                    int neLat = bb.getInt(offset);
                    offset += 4;
                    int swLon = bb.getInt(offset);
                    offset += 4;
                    int swLat = bb.getInt(offset);
                    offset += 4;
                    record.put("block_length", blockLength);
                    record.put("ne_lon", neLon / 100000.0);
                    record.put("ne_lat", neLat / 100000.0);
                    record.put("sw_lon", swLon / 100000.0);
                    record.put("sw_lat", swLat / 100000.0);
                } else if (type == 0 || type == 2 || type == 3) {
                    int length = bb.getInt(offset);
                    offset += 4;
                    int lon = bb.getInt(offset);
                    offset += 4;
                    int lat = bb.getInt(offset);
                    offset += 4;
                    record.put("length", length);
                    record.put("lon", lon / 100000.0);
                    record.put("lat", lat / 100000.0);
                    int strOffset = offset;
                    java.util.List<String> strings = new java.util.ArrayList<>();
                    while (strOffset < offset + (length - 13)) {
                        int strStart = strOffset;
                        while (strOffset < data.length && data[strOffset] != 0) {
                            strOffset += 1;
                        }
                        strings.add(new String(data, strStart, strOffset - strStart, "UTF-8"));
                        strOffset += 1;
                    }
                    record.put("strings", strings);
                    offset += length - 13;
                } else {
                    break;
                }
                records.add(record);
            }
        }
    }

    public void printProperties() {
        for (java.util.Map<String, Object> record : records) {
            System.out.println(record);
            System.out.println("---");
        }
    }

    public void write(String filepath) throws IOException {
        try (FileOutputStream fos = new FileOutputStream(filepath == null ? this.filepath : filepath)) {
            ByteBuffer bb = ByteBuffer.allocate(1024).order(ByteOrder.LITTLE_ENDIAN); // Buffer, resize if needed
            for (java.util.Map<String, Object> record : records) {
                int type = (int) record.get("type");
                fos.write(type);
                if (type == 1) {
                    bb.clear();
                    bb.putInt((int) record.get("block_length"));
                    bb.putInt((int) ((double) record.get("ne_lon") * 100000));
                    bb.putInt((int) ((double) record.get("ne_lat") * 100000));
                    bb.putInt((int) ((double) record.get("sw_lon") * 100000));
                    bb.putInt((int) ((double) record.get("sw_lat") * 100000));
                    fos.write(bb.array(), 0, 20);
                } else if (type == 0 || type == 2 || type == 3) {
                    java.util.List<String> strings = (java.util.List<String>) record.get("strings");
                    byte[] stringsBytes = {};
                    for (String s : strings) {
                        byte[] sBytes = s.getBytes("UTF-8");
                        byte[] temp = new byte[stringsBytes.length + sBytes.length + 1];
                        System.arraycopy(stringsBytes, 0, temp, 0, stringsBytes.length);
                        System.arraycopy(sBytes, 0, temp, stringsBytes.length, sBytes.length);
                        temp[temp.length - 1] = 0;
                        stringsBytes = temp;
                    }
                    int length = 1 + 4 + 4 + 4 + stringsBytes.length;
                    bb.clear();
                    bb.putInt(length);
                    bb.putInt((int) ((double) record.get("lon") * 100000));
                    bb.putInt((int) ((double) record.get("lat") * 100000));
                    fos.write(bb.array(), 0, 12);
                    fos.write(stringsBytes);
                }
            }
        }
    }
}
  1. JavaScript class for .OV2:
class OV2Handler {
    constructor(filepath = null) {
        this.filepath = filepath;
        this.records = [];
        if (filepath) {
            // For browser, use fetch or FileReader; here assume node with fs
            const fs = require('fs');
            this.read(fs.readFileSync(filepath));
        }
    }

    read(data) {
        const dataView = new DataView(data.buffer || data);
        let offset = 0;
        while (offset < data.byteLength) {
            const type = dataView.getUint8(offset);
            offset += 1;
            let record = { type };
            if (type === 1) {
                record.block_length = dataView.getInt32(offset, true);
                offset += 4;
                record.ne_lon = dataView.getInt32(offset, true) / 100000;
                offset += 4;
                record.ne_lat = dataView.getInt32(offset, true) / 100000;
                offset += 4;
                record.sw_lon = dataView.getInt32(offset, true) / 100000;
                offset += 4;
                record.sw_lat = dataView.getInt32(offset, true) / 100000;
                offset += 4;
            } else if (type === 0 || type === 2 || type === 3) {
                record.length = dataView.getInt32(offset, true);
                offset += 4;
                record.lon = dataView.getInt32(offset, true) / 100000;
                offset += 4;
                record.lat = dataView.getInt32(offset, true) / 100000;
                offset += 4;
                let strOffset = offset;
                record.strings = [];
                while (strOffset < offset + (record.length - 13)) {
                    let str = '';
                    let charCode = dataView.getUint8(strOffset);
                    while (charCode !== 0 && strOffset < data.byteLength) {
                        str += String.fromCharCode(charCode);
                        strOffset += 1;
                        charCode = dataView.getUint8(strOffset);
                    }
                    strOffset += 1;
                    if (str) record.strings.push(str);
                }
                offset += record.length - 13;
            } else {
                break;
            }
            this.records.push(record);
        }
    }

    printProperties() {
        this.records.forEach(record => {
            console.log(record);
            console.log('---');
        });
    }

    write() {
        // For node, write to file; here return buffer
        let buffers = [];
        this.records.forEach(record => {
            let type = record.type;
            buffers.push(new Uint8Array([type]).buffer);
            if (type === 1) {
                let dv = new DataView(new ArrayBuffer(20));
                dv.setInt32(0, record.block_length, true);
                dv.setInt32(4, Math.round(record.ne_lon * 100000), true);
                dv.setInt32(8, Math.round(record.ne_lat * 100000), true);
                dv.setInt32(12, Math.round(record.sw_lon * 100000), true);
                dv.setInt32(16, Math.round(record.sw_lat * 100000), true);
                buffers.push(dv.buffer);
            } else if (type === 0 || type === 2 || type === 3) {
                let stringsBytes = new Uint8Array(record.strings.reduce((acc, s) => acc.concat([...new TextEncoder().encode(s), 0]), []));
                let length = 1 + 4 + 4 + 4 + stringsBytes.length;
                let dv = new DataView(new ArrayBuffer(12));
                dv.setInt32(0, length, true);
                dv.setInt32(4, Math.round(record.lon * 100000), true);
                dv.setInt32(8, Math.round(record.lat * 100000), true);
                buffers.push(dv.buffer);
                buffers.push(stringsBytes.buffer);
            }
        });
        return new Blob(buffers).arrayBuffer(); // or write to file in node
    }
}
  1. C class for .OV2:

In C, we use structs instead of classes, but here's a simple implementation using structs and functions.

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <stdint.h>

typedef struct {
    uint8_t type;
    int32_t length; // for type 0,2,3
    double lon;
    double lat;
    char** strings; // array of strings
    int num_strings;
    int32_t block_length; // for type 1
    double ne_lon;
    double ne_lat;
    double sw_lon;
    double sw_lat;
} OV2Record;

typedef struct {
    char* filepath;
    OV2Record* records;
    int num_records;
} OV2Handler;

OV2Handler* ov2_create(const char* filepath) {
    OV2Handler* handler = malloc(sizeof(OV2Handler));
    handler->filepath = strdup(filepath);
    handler->records = NULL;
    handler->num_records = 0;
    if (filepath) {
        ov2_read(handler);
    }
    return handler;
}

void ov2_read(OV2Handler* handler) {
    FILE* f = fopen(handler->filepath, "rb");
    if (!f) return;
    fseek(f, 0, SEEK_END);
    long size = ftell(f);
    fseek(f, 0, SEEK_SET);
    uint8_t* data = malloc(size);
    fread(data, 1, size, f);
    fclose(f);

    int offset = 0;
    while (offset < size) {
        uint8_t type = data[offset];
        offset += 1;
        handler->records = realloc(handler->records, (handler->num_records + 1) * sizeof(OV2Record));
        OV2Record* rec = &handler->records[handler->num_records];
        memset(rec, 0, sizeof(OV2Record));
        rec->type = type;
        if (type == 1) {
            memcpy(&rec->block_length, data + offset, 4);
            offset += 4;
            int32_t ne_lon_int;
            memcpy(&ne_lon_int, data + offset, 4);
            rec->ne_lon = ne_lon_int / 100000.0;
            offset += 4;
            int32_t ne_lat_int;
            memcpy(&ne_lat_int, data + offset, 4);
            rec->ne_lat = ne_lat_int / 100000.0;
            offset += 4;
            int32_t sw_lon_int;
            memcpy(&sw_lon_int, data + offset, 4);
            rec->sw_lon = sw_lon_int / 100000.0;
            offset += 4;
            int32_t sw_lat_int;
            memcpy(&sw_lat_int, data + offset, 4);
            rec->sw_lat = sw_lat_int / 100000.0;
            offset += 4;
        } else if (type == 0 || type == 2 || type == 3) {
            memcpy(&rec->length, data + offset, 4);
            offset += 4;
            int32_t lon_int;
            memcpy(&lon_int, data + offset, 4);
            rec->lon = lon_int / 100000.0;
            offset += 4;
            int32_t lat_int;
            memcpy(&lat_int, data + offset, 4);
            rec->lat = lat_int / 100000.0;
            offset += 4;
            int str_offset = offset;
            rec->num_strings = 0;
            rec->strings = NULL;
            while (str_offset < offset + (rec->length - 13)) {
                int str_start = str_offset;
                while (str_offset < size && data[str_offset] != 0) str_offset += 1;
                int str_len = str_offset - str_start;
                rec->strings = realloc(rec->strings, (rec->num_strings + 1) * sizeof(char*));
                rec->strings[rec->num_strings] = malloc(str_len + 1);
                memcpy(rec->strings[rec->num_strings], data + str_start, str_len);
                rec->strings[rec->num_strings][str_len] = '\0';
                rec->num_strings += 1;
                str_offset += 1;
            }
            offset += rec->length - 13;
        } else {
            break;
        }
        handler->num_records += 1;
    }
    free(data);
}

void ov2_print_properties(OV2Handler* handler) {
    for (int i = 0; i < handler->num_records; i++) {
        OV2Record rec = handler->records[i];
        printf("Type: %d\n", rec.type);
        if (rec.type == 1) {
            printf("Block Length: %d\n", rec.block_length);
            printf("NE Longitude: %f\n", rec.ne_lon);
            printf("NE Latitude: %f\n", rec.ne_lat);
            printf("SW Longitude: %f\n", rec.sw_lon);
            printf("SW Latitude: %f\n", rec.sw_lat);
        } else {
            printf("Length: %d\n", rec.length);
            printf("Longitude: %f\n", rec.lon);
            printf("Latitude: %f\n", rec.lat);
            printf("Strings: ");
            for (int j = 0; j < rec.num_strings; j++) {
                printf("%s ", rec.strings[j]);
            }
            printf("\n");
        }
        printf("---\n");
    }
}

void ov2_write(OV2Handler* handler, const char* filepath) {
    FILE* f = fopen(filepath ? filepath : handler->filepath, "wb");
    if (!f) return;
    for (int i = 0; i < handler->num_records; i++) {
        OV2Record rec = handler->records[i];
        fwrite(&rec.type, 1, 1, f);
        if (rec.type == 1) {
            fwrite(&rec.block_length, 4, 1, f);
            int32_t ne_lon_int = (int32_t)(rec.ne_lon * 100000);
            fwrite(&ne_lon_int, 4, 1, f);
            int32_t ne_lat_int = (int32_t)(rec.ne_lat * 100000);
            fwrite(&ne_lat_int, 4, 1, f);
            int32_t sw_lon_int = (int32_t)(rec.sw_lon * 100000);
            fwrite(&sw_lon_int, 4, 1, f);
            int32_t sw_lat_int = (int32_t)(rec.sw_lat * 100000);
            fwrite(&sw_lat_int, 4, 1, f);
        } else {
            int32_t length = rec.length;
            fwrite(&length, 4, 1, f);
            int32_t lon_int = (int32_t)(rec.lon * 100000);
            fwrite(&lon_int, 4, 1, f);
            int32_t lat_int = (int32_t)(rec.lat * 100000);
            fwrite(&lat_int, 4, 1, f);
            for (int j = 0; j < rec.num_strings; j++) {
                fwrite(rec.strings[j], strlen(rec.strings[j]), 1, f);
                uint8_t null_byte = 0;
                fwrite(&null_byte, 1, 1, f);
            }
        }
    }
    fclose(f);
}

void ov2_destroy(OV2Handler* handler) {
    for (int i = 0; i < handler->num_records; i++) {
        for (int j = 0; j < handler->records[i].num_strings; j++) {
            free(handler->records[i].strings[j]);
        }
        free(handler->records[i].strings);
    }
    free(handler->records);
    free(handler->filepath);
    free(handler);
}