<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:content="http://purl.org/rss/1.0/modules/content/">
  <channel>
    <title>Konstantinos Fragkoulis</title>
    <link>https://kfragkoulis.com/</link>
    <description>ECE student building embedded systems, FPGAs, and hardware.</description>
    <language>en</language>
    <lastBuildDate>Mon, 10 Aug 2026 12:00:00 +0000</lastBuildDate>
    <atom:link href="https://kfragkoulis.com/feed.xml" rel="self" type="application/rss+xml"/>
    <item>
      <title>How I built a private travel journal for iPhone</title>
      <link>https://kfragkoulis.com/blog/building-travel-log/</link>
      <guid isPermaLink="true">https://kfragkoulis.com/blog/building-travel-log/</guid>
      <pubDate>Mon, 10 Aug 2026 12:00:00 +0000</pubDate>
      <description>How Travel Log turns occasional location updates and photo metadata into places, trips, and an exploration map completely on device.</description>
      <content:encoded><![CDATA[<p>I have always enjoyed traveling. After visiting more than twelve countries, most of them several times, I started noticing a problem: I remembered the important moments, but the shape of each trip was slowly becoming less clear.</p>
<p>I wanted to know exactly when I was in a city. I wanted to remember which parts of it I had explored, which photos belonged to that visit, and how one stop connected to the next. More than anything, I wanted one place that could hold this history without asking me to record every trip manually.</p>
<p>I tried a few different solutions. Some travel apps let me mark countries and cities manually. They produced a nice map, but they did not really show anything more than a list of places I have visited. Apple Photos came closer because it already had my pictures, their dates, and often their locations. Its trip grouping, however, was a bit hit or miss. I sometimes found trips that were missing, split, or joined together in ways that did not match what happened.</p>
<p>For a while, one of the most accurate records of my travels came from Revolut (yes, the neobank). It would track payments made in other countries and estimate when I had traveled. I liked the presentation, but a banking app is not the right place, nor the right tool to keep a record of your life. What if you pay cash? What if you use a card from another bank? What if they remove the feature at some point? You really don&rsquo;t need many reasons to realize that this is a bad idea.</p>
<p>Each option had some piece of what I wanted. None of them had the whole thing.</p>
<p>This led me to build <strong>Travel Log</strong>, a private travel journal for iPhone. It receives occasional location updates in the background, figures out the places where I stayed, groups those stays into trips, and builds a map of the places I have explored. It can also go through the metadata of my photos to infer places that I have visited and reconstruct trips from before the app was even installed.</p>
<figure class="post-screenshot">
  <img src="/blog/building-travel-log/travel-log-trips.webp" alt="Travel Log's Trips screen showing an ongoing trip to Portugal and Madeira and a list of past trips" width="1080" height="2347" decoding="async">
  <figcaption>Travel Log running with fictional sample data.</figcaption>
</figure>

<p>The app is currently in beta for iOS 26 or newer. It works best on Apple Intelligence enabled devices (iPhone 15 Pro or newer), but it can work on any iOS 26 supported device. If you would like to test it yourself, you can <a href="https://testflight.apple.com/join/GPKM3eQT">join the TestFlight beta</a>. It requires “Always” location access for its main features. Photo access, calendar access, notifications, and iCloud sync are optional. In fact, the app does not require an internet connection and almost all of the features were designed to work primarily offline.</p>
<h2>Starting with an imperfect signal</h2>
<p>The simplest version of a location journal would record GPS coordinates all day. That would give us very accurate location data, but it comes with two major problems: it drains your battery and iOS requires the app to show a blue location arrow in the Dynamic Island.</p>
<p>Travel Log uses a different part of Core Location called <a href="https://developer.apple.com/documentation/corelocation/cllocationmanager/startmonitoringsignificantlocationchanges()">Significant Location Change</a> (SLC). Instead of keeping the app alive and asking for a constant stream of location updates, it registers with iOS and lets the operating system decide when the phone has moved far enough to send an update (Apple says &gt;500 meters, &gt;5 min from the last update, but that&rsquo;s a minimum. Updates could come less often).</p>
<p>This system enables the most important feature of the app: you truly set it and forget it. There is no start button, no manual recording of trips, no requirement to keep the app open, no visible indicator, no nothing. You install the app, finish the setup, force close it, and go on with your day. In my own testing, the app keeps receiving SLC updates even after a device reboot, without any interaction from the user.</p>
<p>iOS is still in control of background activity and there is no guarantee that this will keep working forever. You can ultimately choose to disable background app refresh, or iOS may decide to stop delivering updates if the app is not used at all.</p>
<p>With that in mind, we end up with an app that gets sparse location updates rather than a detailed route. It may know that I was near my home, later near the airport, and later in another city. It will not necessarily know every road I took between them. That limitation shaped almost every decision that followed.</p>
<p>When iOS sends a location update, the app first checks whether it is usable. Old readings are ignored. Readings with extremely poor accuracy are also ignored. A reading that appears to come from high altitude can be discarded when the altitude itself looks trustworthy. This helps with photos and location updates recorded during flights, although it&rsquo;s not perfect.</p>
<p>The remaining location is saved as a ping. A ping is simply one observation: a time, a pair of coordinates, an accuracy value, and a few details about where it came from. The app also places it inside one of the hexagon shaped areas used by <a href="https://h3geo.org/">Uber&rsquo;s H3 hexagonal grid</a>. Those hexagons later become the exploration map.</p>
<h2>Turning pings into places</h2>
<p>A single ping does not tell us much. It only means that the phone was there at one moment in time. Travel Log needs to join nearby pings together before they become useful.</p>
<p>The app reads unprocessed pings in the order they arrived and builds what it calls a stay. A stay is a period spent in roughly the same area. If a new ping is more than 3 kilometers from the center of the current stay, a new stay begins. The same thing happens when more than 4 hours have passed since the previous ping. The center of a stay is simply the running average of all the pings that make up that stay.</p>
<p>An important realization is that a lack of pings usually means that the user has not moved to a new location. When you go back to your hotel in the evening for example, Travel Log may receive a location update, but throughout the night it does not receive any new updates. The next update may arrive in the morning when you are already leaving.</p>
<p>When that happens, the app extends your hotel stay halfway into the quiet period, but never by more than 12 hours. That limit matters even more for photo imports, where gaps can be much longer and the app cannot know whether you stayed in one place or simply stopped taking geotagged photos.</p>
<p>At this point the app has turned pings into stays, but it still hasn&rsquo;t constructed a single trip.</p>
<h2>Home is not a circle</h2>
<p>Home is the reference point for the entire trip system. A trip begins when you leave your home area and ends when you return. If that area is too small, an ordinary visit to work or the gym becomes a trip. If it is too large, a real journey disappears into home.</p>
<p>My first version used a circle around the location the user marked as home. That sounds reasonable until you look at a real city. Daily life is not circular. In my case, a circle large enough to include the places I regularly visit around Athens also included the airport. A smaller circle excluded parts of the city while still failing to describe its actual shape.</p>
<p>Travel Log now starts with the measured boundary of the built up city that contains the user&rsquo;s home. The boundary comes from the geographic database already included with the app. If the app cannot find a city boundary, the home area begins as a small circle around the chosen location.</p>
<p>The area can then learn from the user&rsquo;s routine. A place can become part of home after at least three qualifying returns within twelve months. Each visit must involve meaningful time at the place and a return home on the same day. Airport stops and other transit do not count. This lets an office, a weekly market, or a relative&rsquo;s house become part of ordinary life without allowing a frequently visited holiday destination to become home.</p>
<p>This learned area is not permanent. If the return pattern disappears for long enough, that place eventually stops counting as home. The area can therefore expand as a routine develops and shrink after the routine ends.</p>
<p>The user remains the final authority. A missed nearby journey can be marked as a trip. A false trip can be marked as part of home. Learned places are visible in Settings and can be removed. Travel Log also keeps homes tied to time, so moving to a new city does not change how an older trip is interpreted.</p>
<h2>Working out what belongs to a trip</h2>
<p>The user tells Travel Log where home is and can optionally add routine places such as an office or a gym. A stay inside one of those areas is easy to understand. It is classified immediately and it doesn&rsquo;t get any further attention.</p>
<p>The difficult stays are the ones in between. Three hours near an airport could be a layover. Three hours in a small town could be a lunch stop on a road trip, or it could be the main reason for the journey. Duration alone is not enough. Distance from home is not enough. The places before and after a stay matter a lot.</p>
<p>For these cases, Travel Log can use the on device large language model that Apple provides on compatible devices through the <a href="https://developer.apple.com/documentation/foundationmodels">Foundation Models framework</a>. The model receives a short list of stays with their places, times, durations, distance from home, and nearby airports. It then labels each stay as either a destination or transit.</p>
<p>The output has a fixed shape:</p>
<div class="highlight"><pre><span></span><code><span class="p">@</span><span class="n">Generable</span>
<span class="kd">struct</span><span class="w"> </span><span class="nc">SingleStayClassification</span><span class="w"> </span><span class="p">{</span>
<span class="w">    </span><span class="kd">var</span><span class="w"> </span><span class="nv">stayIndex</span><span class="p">:</span><span class="w"> </span><span class="nb">Int</span>

<span class="w">    </span><span class="p">@</span><span class="n">Guide</span><span class="p">(.</span><span class="n">anyOf</span><span class="p">([</span><span class="s">&quot;destination&quot;</span><span class="p">,</span><span class="w"> </span><span class="s">&quot;transit&quot;</span><span class="p">]))</span>
<span class="w">    </span><span class="kd">var</span><span class="w"> </span><span class="nv">category</span><span class="p">:</span><span class="w"> </span><span class="nb">String</span>
<span class="p">}</span>
</code></pre></div>

<p>This avoids asking the model for a block of text and then trying to parse it. The result must contain a stay number and one of two allowed categories.</p>
<p>I deliberately give the model very little authority. It can offer an opinion about an unclear stay. It cannot decide where a trip begins or ends.</p>
<p>Trip boundaries use a much simpler rule: a trip starts when the user leaves their home and ends when they come back home. If there is no home stay between two travel stays, they remain part of the same trip. This handles journeys with several destinations without having to understand the entire structure of a trip.</p>
<p>(This works great for most trips, but it doesn&rsquo;t properly handle some specific cases. Imagine studying abroad for a semester and going back home for a weekend. The semester gets cut in half and becomes two separate trips. The opposite happens if you take a weekend trip to another city: because you never return to your actual home, the app treats it as part of the larger study abroad trip. This can be fixed with &ldquo;temporary homes&rdquo;: places that are still travel destinations, but can also act as the start and end of separate, &ldquo;nested&rdquo; trips. This will come in an update soon™).</p>
<p>There are also ordinary rules around the model. A very short stay is usually transit. A short airport stay is usually transit. A stay lasting several days should not remain classified as transit just because the model made a poor decision. If the user corrects a stay manually, the app treats that correction as final.</p>
<p>Apple Intelligence is not available on every device that can run iOS 26. It can also be turned off or still be downloading. When the model is unavailable, Travel Log uses a smaller set of normal rules. The result may be less subtle, but the main app still works.</p>
<h2>The limitations of a 3B parameter model</h2>
<p>Classification was not the only place where I used the model. I also wanted short trip titles. A trip that says “Boston &amp; NYC” is easier to process than “Boston, Cambridge, Brooklyn, Queens, and every other place recorded during the trip.”</p>
<p>Early results showed why these smaller models need very carefully crafted prompts. I was giving the model way too much information and that led to it adding the capital of the country to the title of each trip. Mallorca became &ldquo;Mallorca &amp; Madrid&rdquo; and Thessaloniki became &ldquo;Thessaloniki &amp; Athens&rdquo;.</p>
<p>I tried improving the prompt, but prompts alone did not make the result trustworthy. The real fix was a validator. Every important word in the title must now come from the actual places stored in the trip, or from a small list of allowed joining words. If the model names a place that is not present, the title is rejected.</p>
<p>Transit places are also removed from the context given to the model. An eight hour connection at Heathrow repeatedly encouraged the model to put London in the title of a trip to Los Angeles. Removing the airport stay from the prompt worked better than repeatedly telling the model to ignore it.</p>
<p>The app gives the model two attempts before building a simple title from the main destinations.</p>
<h2>Giving coordinates a name</h2>
<p>&ldquo;37.1° N, 25.38° E&rdquo; doesn&rsquo;t tell you much, but this is what the app has been working with up until now. A travel journal needs to turn coordinates into names such as “Naxos, Greece.” This process is called reverse geocoding.</p>
<p>Apple provides a service for it, and it works well when an app uses it occasionally. Travel Log may need to process tens of thousands of old photos in one import. Sending each location through an online service would be slow, would depend on the network, and would get rate limited.</p>
<p>I decided to include a geographic database inside the app. It is a read only SQLite file of about 125 MB, built from <a href="https://www.geonames.org/">GeoNames</a>, <a href="https://www.naturalearthdata.com/">Natural Earth</a>, <a href="https://human-settlement.emergency.copernicus.eu/ghs_fua.php">GHS-FUA</a>, <a href="https://human-settlement.emergency.copernicus.eu/ghs_ucdb_2024.php">GHS-UCDB</a>, and <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a>, with <a href="https://www.wikidata.org/">Wikidata</a> cross-references for islands. The app also bundles airport data from <a href="https://ourairports.com/">OurAirports</a> for transit detection.</p>
<p>The database can answer several questions for one coordinate. It can find a nearby city, a neighborhood, an island, a larger metropolitan area, a region, and a country. The app can show the specific place on a stay while using the larger city name for a trip title.</p>
<p>There is a cost here: a larger app download. I think it is massively outweighed by the speed, throughput, and privacy of local lookups. They happen offline and are never rate limited, while Apple&rsquo;s geocoder remains available as a fallback.</p>
<h2>Reconstructing trips from photos</h2>
<p>Passive tracking solves current and future trips. It does nothing for the years before the app existed.</p>
<p>Your photo library most likely contains much of that history. Photos usually come with a timestamp and location data. Travel Log can read that information, create historical pings, and send them through the same stay and trip pipeline used for live tracking.</p>
<p>The importer works in small batches and remembers its progress, so it can stop and continue later without losing photos or creating duplicates.</p>
<p>The importer ignores screenshots, photos without a useful location, impossible dates, very inaccurate coordinates, and some high altitude photos that were probably taken from a plane. It also skips shared photos.</p>
<h2>Exploring the world but not the sky</h2>
<p>The exploration map is the part of the app where I spend the most time. Each accepted location contributes to an H3 hexagon. Over time, the map fills and you see a hexagonal grid that changes color based on how many times you have visited each hexagon.</p>
<p>My first attempt hid every area that belonged only to transit. The intention was reasonable. A location update from a plane should not make it look as though I explored the country below it.</p>
<p>Then a road trip disappeared from the map.</p>
<p>Road trips can produce a chain of single-ping stays along the way. Many are too short to look like destinations, so my first rule could hide the drive in exactly the same way it hid a flight.</p>
<p>The current rule looks for evidence of air travel instead of hiding transit in general. A long distance, an airport near one end, and a speed that is not realistic on the ground can identify a likely flight. Areas connected only to flying or airport stops can be hidden. Drives and ferry journeys remain visible.</p>
<p>The map and the statistics screen use the same function to decide whether an area counts. This matters more than it sounds. Two parts of a personal journal should not disagree about how much of the world the user has explored.</p>
<figure class="post-screenshot">
  <img src="/blog/building-travel-log/travel-log-passport.webp" alt="Travel Log's passport statistics and exploration globe showing sample travel across several continents" width="1080" height="2347" loading="lazy" decoding="async">
  <figcaption>The passport and exploration globe, using the same sample history.</figcaption>
</figure>

<h2>Keeping the data safe</h2>
<p>A detailed travel history is sensitive. I would certainly not want to send my entire location history over the years to some random guy&rsquo;s server.</p>
<p>Travel Log has no account system and no server of its own. The main database stays on device, while place name lookup, trip building, search, and model requests all happen there. There are no advertising tools or analytics.</p>
<p>iCloud sync is optional and uses the user&rsquo;s private CloudKit database. WeatherKit and the rare online place name lookup send requests directly from the device to Apple services.</p>
<p>As I said before, Travel Log works perfectly offline because it was built with privacy in mind.</p>
<h2>What the app still gets wrong</h2>
<p>Travel Log receives snapshots of your daily life, not a precise record of every movement. Significant Location Change is occasional and controlled by iOS. A short visit may produce no useful update. A route may have gaps. A location received during a flight may sometimes survive the filters because the phone did not report a reliable altitude.</p>
<p>Photo import depends on the information stored with the photos. Images without a location cannot create places. A sparse library may reconstruct the broad shape of a trip while missing some stops or estimating dates imperfectly.</p>
<p>The language model can also make poor decisions. The app limits where those decisions matter, but I still expect many issues to surface as more people test the app. People who live near a border, move between several homes, work at an airport, or travel for long periods are especially valuable testers because their lives challenge simple rules.</p>
<p>The app currently requires iOS 26 or newer. The model features need a device where Apple Intelligence is available, although the rest of the travel pipeline has normal fallbacks.</p>
<p>I have not made the app open source. I may publish smaller parts later, but I do not want that decision to delay the beta.</p>
<h2>What now?</h2>
<p>I&rsquo;ve been using the app while building it over the past ~6 months. It has worked well on my phone and with my travel history. That does not mean it will work well for everybody, even though I&rsquo;d hope it does.</p>
<p>If what you read fascinates you, if you travel a lot, if you&rsquo;d like to try something new, or if it just sounds interesting, you can <a href="https://testflight.apple.com/join/GPKM3eQT">join the Travel Log TestFlight beta</a>. Feedback can be sent through TestFlight or by email at <a href="mailto:travellog@kfragkoulis.com">travellog@kfragkoulis.com</a>.</p>
<p>This is still a beta. It is not perfect, and some of its guesses will be wrong. I try to ship updates as frequently as possible. The App Store release should come in a few days/weeks.</p>]]></content:encoded>
    </item>
  </channel>
</rss>
