Learn · Mobile forensics

Location data

Phones record where they are, often without the user noticing. Reading those records correctly needs three things: the right epoch, the right time zone, and the accuracy radius of every point.

1 Where location data lives

SourceWhat it holdsNotes
iOS location cachesRecent fixes with time, latitude, longitude, accuracy, sometimes speed and courseSQLite databases of the system location services (e.g. the Cache.sqlite family under routined); Core Data style tables with Z prefixes and Cocoa timestamps. Names and retention change between iOS versions.
iOS significant locationsPlaces the phone visits often, with visit timesLearned by the system; protected, usually only in full file-system acquisitions
Google location historyTimeline of positions and visitsOlder exports via Google Takeout (Records.json with latitudeE7); newer versions keep the timeline on the device
App databasesCheck-ins, ride and fitness tracks, shared locations in chats, map searchesEach app has its own schema and epoch
Media metadataGPS coordinates in photo and video filesDepends on camera settings; often removed when files are shared
Network recordsCell towers and Wi-Fi networks seenPositions are approximate and must be resolved through a tower or Wi-Fi database

The sample on this page is a small location cache modelled on the Core Data style: a ZLOCATION table with one row per fix, recorded during a morning jog around a lake in a public park.

2 A location cache in hex

Each row has ZTIMESTAMP, ZLATITUDE, ZLONGITUDE, ZHORIZONTALACCURACY (metres), ZSPEED and ZSOURCE. The coordinates are 8-byte floats (serial type 7). Cell 1 has ZTIMESTAMP 810369000.5: far too small for Unix time today, and exactly right for Cocoa time, seconds since 2001-01-01 00:00 UTC.

Unix seconds = Cocoa seconds + 978,307,200 810,369,000.5 + 978,307,200 = 1,788,676,200.5 → 2026-09-06 06:30:00.5 UTC

Look at the accuracy column: the table declares it REAL, but most cells store it with serial type 1 (a 1-byte integer). SQLite saves space by storing a REAL without a fractional part as an integer. The values are the same; only the bytes differ. Cell 13 stores 1400 in two bytes, a 1,400 m radius from a cell tower fix.

The same rows as a query (builder output)

Try this: select the ZLATITUDE of cell 1 and read its 8 bytes as a big-endian IEEE 754 double (any online or Python converter: struct.unpack('>d', bytes.fromhex('…'))). Then check how many seconds lie between two fixes.

3 Epochs and time zones

A timestamp is a number of units since an epoch. The number alone does not tell you which epoch. Decide by the source (the platform and the app), then check that the result is plausible.

FormatEpoch and unitTypical value in 2026Where
Unix time1970-01-01, seconds1.79 × 109 (10 digits)many apps, Linux
Unix milliseconds1970-01-01, ms1.79 × 1012 (13 digits)most Android databases, Java
Cocoa / Mac absolute time2001-01-01, seconds (often with fraction)8.1 × 108 (9 digits)iOS and macOS
WebKit / Chrome time1601-01-01, microseconds1.34 × 1016 (17 digits)Chrome history, Chromium-based apps

Timestamp converter

All four formats count in UTC. The phone's time zone is not part of the value. When you report local times, state the zone and remember daylight saving time: the same UTC value is 07:30 in London in September and 06:30 in December. Some apps store local time without a zone, or keep the zone in a separate column; find out before you convert. Device clocks can also be wrong, so compare with an independent time source if timing is critical.

4 Every point is a circle

A fix is not a dot. It is a circle: "the device was probably within r metres of this point". GPS fixes outdoors reach a few metres. Positions computed from Wi-Fi networks are tens of metres off. Cell tower positions can be off by a kilometre or more. Mixed into one track, the coarse fixes create jumps and detours that never happened.

GPSWi-FicellTakeout

The track above is drawn from the bytes of the sample page: no map service, so you only see the geometry. Fix 13, the cell fix, has a 1,400 m radius; its circle covers the whole park. Fix 8 (Wi-Fi, ±65 m) pulls the track off the path at the western end. Untick "accuracy circles" and the track looks precise, which it is not.

Takeout-style exports

Google's older location history exports store coordinates as integers in units of 10−7 degrees (latitudeE7, longitudeE7), with ISO 8601 timestamps in UTC. Divide by 10,000,000 to get degrees. The squares on the map are these records: the walk to the park before the jog.

Takeout-style JSON (sample)
Before you put a point on a map in a report: give the source, the accuracy radius, the time in UTC and local time, and how the position was obtained (GPS, Wi-Fi, cell). Never report a cell or Wi-Fi fix as "the phone was at this address".

Try this: convert the first Takeout record to degrees. How far is it from the start of the jog, and how many minutes passed between them? Does the walking speed make sense?

5 Try it yourself

Answer from the hex view, the converter and the Takeout sample above. Numbers can be typed in decimal or hex (0x…).

Convert the ZTIMESTAMP of the last fix (cell 16) to Unix seconds.

Select ZTIMESTAMP in cell 16 to see its value, then add the offset between the two epochs (section 2).

The 8 bytes 41 C8 26 A2 BC C0 00 00, read as a big-endian double, are 810,370,425.5 Cocoa seconds.

810,370,425.5 + 978,307,200 = 1,788,677,625.5 → 2026-09-06 06:53:45.5 UTC.

How many seconds lie between fix 1 and fix 2?

Subtract the two ZTIMESTAMP values. Both are Cocoa seconds, so the epoch does not matter for a difference.

Fix 1: 41 C8 26 9F F4 40 00 00 = 810,369,000.5. Fix 2: 41 C8 26 A0 23 C0 00 00 = 810,369,095.5.

Difference: 95 seconds. All fixes in the table follow the same 95-second rhythm.

Fix 8 (Wi-Fi) has no speed. Which serial type byte does its ZSPEED have?

Cell 8's record header has 8 bytes: its own length, then one serial type per column (Z_PK, ZTIMESTAMP, ZLATITUDE, ZLONGITUDE, ZHORIZONTALACCURACY, ZSPEED, ZSOURCE).

Cell 8 starts 26 08 08 00 07 07 07 01 01 15: payload 38, rowid 8, then the record header 08 | 00 07 07 07 01 01 15.

ZSPEED is the 6th serial type: 01, a 1-byte signed integer. Its value byte is FF = −1, a "no value" marker, not a speed. In cell 1 the same column has type 07 (an 8-byte float, 2.8 m/s).

What is the latitude of the first Takeout record, in degrees?

Open the Takeout-style JSON in section 4. latitudeE7 is in units of 10−7 degrees.

"latitudeE7": 515134000 ÷ 10,000,000 = 51.5134°. The longitude −1,588,000 gives −0.1588°.

These 46 bytes are cell 1, the first fix (file offset 0x7D2). Its ZTIMESTAMP is the big-endian double 41 C8 26 9F F4 40 00 00 at 0x7DC. Edit the bytes and watch cell 1 in the decoded page below:

  • Change the 4th timestamp byte 9F (at 0x7DF) to A0: the time moves by 512 seconds. Reset and change the 3rd byte 26 (at 0x7DE) to 27 instead. How far does it move now? Each byte to the left is worth 256 times more.
  • Reset, then change the 6th timestamp byte 40 (at 0x7E1) to 00. The half second disappears, and the decoder now also offers a Unix reading. Why can a whole number of seconds not tell you its epoch?
  • Set ZHORIZONTALACCURACY, the single byte 05 at 0x7F4, to FF. Serial type 1 is signed: the radius becomes −1, which is not a radius at all.