Here is a complaint I get in one form or another every few weeks: the app says I was in one place for three and a half hours, but I was really at two places in that window, and they were nowhere near each other.

The interesting part is not the missing place. It is what you find when you look at the rest of the day around it. Some of those visits have a departure the phone actually reported. Others simply do not, and the app has quietly worked out an ending for them. Which visits fall into which group is not something you can predict, and on a bad day it is most of them.

One day of visits as iOS reports it An illustrative day. All three arrivals were reported. Only the first departure was; the other two endings had to be worked out. WHAT THE PHONE REPORTED arrived 08:20 arrived 10:05 left 09:50 arrived 16:40 Three arrivals. One departure. The other two never came. WHAT THE TIMELINE HAS TO SHOW Home ends: reported Bookshop ends: guessed Home now*
An illustrative day. Solid edge = reported by the phone. Dashed edge = worked out afterwards. The first visit was closed properly. The other two were not, and nothing about the day tells you why.

What iOS actually gives you

If you want to know where someone spent their day on iOS, the mechanism is CLVisit. The system decides on its own that a stop was meaningful and hands you an object with an arrival date and a departure date. It is genuinely clever, it costs almost no battery, and it is the right tool.

It is also lopsided, and unevenly so. Arrivals come through dependably. Departures are the unreliable half: plenty of visits are closed properly, and then some are delivered with departureDate set to Date.distantFuture — the framework’s way of saying still there, as far as I know. Sometimes the departure follows minutes later. Sometimes it never arrives at all, and the only evidence you ever get that the person left is that they turned up somewhere else.

That unevenness is the part that shapes the design. If departures were always missing you could treat every ending as unknown and be done. If they were always present there would be no problem to solve. Because it varies — visit to visit, day to day, with no signal telling you which kind you are holding — every visit has to be handled as though its ending might be a guess.

So a full day of movement reaches your app as a handful of opening brackets and almost no closing ones.

The tempting mistake

The obvious move is to close the bracket yourself. You know when the next arrival came in, so write that into the previous visit’s departure and move on. Now every visit has a start and an end, the durations add up, and the database looks clean.

It looks clean because you have overwritten the difference between something you were told and something you decided. Six months later nobody — including you — can tell which visit durations came from the operating system and which ones your own code invented. A timeline built that way is not a record. It is a plausible reconstruction wearing a record’s clothes.

And the failure mode is not subtle. Leave a visit unclosed, sync a device that has been quiet for a couple of days, and you get this:

08:20 AM – 07:45 AM  (41h 25m)

An end before its start, and a stay of nearly two days. Both numbers are arithmetically correct: the visit really did begin on one morning and the next known event really was two mornings later. What is wrong is the presentation: the row showed bare clock times with no dates, so a two-day span rendered as nonsense. That one is our bug, and it is fixed — the row now says +2d.

Infer late, and say so

What we settled on is a rule that sounds fussy and has repaid itself many times: never persist a guess.

Storage keeps exactly what the system said, sentinel and all. When a visit needs an end time for display, it is computed at that moment from the earliest of the next visit’s arrival and the next drive’s departure. If neither exists, the visit is still open and says so.

Two things fall out of this. The first is that the record stays honest: the stored data is whatever iOS reported, so a later improvement to the inference improves every historical day at once, with no migration and no sweep rewriting years of rows. The second is that the app can mark its own uncertainty. Every inferred value in the interface carries an asterisk. It is a small mark that quietly says: this is our best reconstruction, not a measurement.

More than one person has worked out their own answer before asking me, simply because the asterisks were there to be read. That is the mark doing its job.

A second trap: averaging your way home

A related problem, and a good illustration of how location data punishes intuition. To decide whether somewhere is interesting, you need to know where the person usually is. The natural approach is to average their recent coordinates and call that the centre.

That works precisely until someone travels. Take one week in Italy and the mean of your coordinates slides hundreds of kilometres across Europe. Now home is four hundred kilometres from “normal” and the trip itself looks unremarkable, because the outlier moved the baseline it was supposed to be measured against. A mean cannot survive outliers, and travel is nothing but outliers.

What works is not a centre at all. Take the places the person returns to repeatedly — home, the office, the gym, the usual shops — and measure distance to the nearest one. A place visited once never becomes an anchor, so no trip can corrupt the baseline. A café next door to the office is ordinary even though it is far from home. Somewhere far from all of them is genuinely away.

The part no algorithm fixes

Back to the complaint at the top. Those two places collapse into one because iOS reports a single arrival for the window. There is no second visit hiding in the data waiting to be separated out. Better inference cannot recover a stop the operating system never noticed — you cannot compute your way out of missing information.

Which leaves one honest answer: let people correct the record. Add the place the phone missed, split the visit that got merged, fix the times. That editor is being built now, because it is the only thing that actually closes the gap.

Most people will never touch it, and that is fine — the point of an automatic journal is that you do not have to maintain it. But the people who care about their own record being right care about it a great deal, and at the moment the app tells them it is guessing without offering them any way to say what actually happened. Correcting a visit will be there for them.

An automatic journal is a claim about accuracy. When the claim fails, the app owes you two things — an admission that it is guessing, and a way to put it right.