Journal d'ingénierie.
Comment Mapsake a créé sa géolocalisation de voyage hors ligne.
La transformation d'une coordonnée photo en un pays, une région, une ville et un aéroport nécessite plus qu'une simple requête du lieu le plus proche. Voici comment Mapsake combine des données de lieux ouvertes, une géométrie de limites, des règles déterministes et une indexation locale.
Une coordonnée n'est pas encore un lieu.
Une photo de voyage peut contenir une latitude et une longitude avec une précision impressionnante. Ces deux chiffres n'indiquent toujours pas si l'appareil photo était au Japon, dans la préfecture de Kyoto, à Kyoto ou à l'aéroport international de Kansai en rentrant chez soi. Mapsake a besoin de cette hiérarchie avant qu'un point ne puisse devenir un enregistrement de voyage utile.
Le composant qui répond à ces questions est un répertoire géographique : un répertoire structuré de lieux géographiques nommés. Mapsake inclut son répertoire géographique dans l'application, ainsi que les lignes de limite utilisées pour dessiner et tester les pays et les régions de premier ordre. La recherche, le marquage manuel, l'importation de photos, les histoires de lieux, les statistiques de Passport, les réalisations et même quelques vues d'amis en dépendent.
Il aurait été plus simple d'envoyer chaque coordonnée à un géocodificateur web. Cela aurait également rendu l'importation de grandes photos plus lente, dépendante du réseau, plus difficile à reproduire et moins privée. L'approche hors ligne a nécessité plus d'ingénierie au départ, mais elle a donné à Mapsake un vocabulaire géographique stable qui fonctionne de la même manière dans un avion, à la maison et des années après qu'un ensemble de données source a changé.
C'est l'histoire de la façon dont cette couche a été construite, où les points et les lignes ne coïncident pas et pourquoi « ville la plus proche » n'est que le début d'une réponse correcte.
Quatre ensembles de données ouverts, quatre emplois différents
Aucune source unique ne contient tout ce dont Mapsake a besoin, c'est pourquoi le processus de construction combine quatre types de données ouvertes :
- GeoNames fournit les pays, les régions administratives de premier ordre, les lieux habités, les identifiants stables, les noms alternatifs, les coordonnées et la population.
- Nos aéroports fournit les aéroports, les codes IATA et OACI, les noms, les municipalités et les coordonnées.
- Natural Earth fournit la géométrie des frontières du pays (admin-1) ainsi que des points d'étiquette de carte utiles.
- Une couche interne à Mapsake enregistre les décisions relatives aux produits, telles que le nombre de pays pris en charge, les alias et les corrections qui ne peuvent pas être déduits de manière fiable à partir d'une source générique.
Chaque source est bonne dans un domaine différent. GeoNames sait qu'une ville appartient à une région et à un pays, mais une coordonnée de ville est un point, pas une frontière. Natural Earth sait où un polygone est dessiné, mais ses identifiants de fonctionnalités ne correspondent pas toujours parfaitement à GeoNames. OurAirports sait que KLAX et LAX font référence au même aéroport, mais ce n'est pas une hiérarchie de tous les endroits autour de l'aéroport.
Le script de construction télécharge et met en cache les fichiers sources, les normalise, valide les relations et génère deux artefacts compilés : une base de données SQLite en lecture seule et une géométrie GeoJSON simplifiée. L'application fournit ces résultats. Elle ne télécharge pas une base de données mondiale au premier lancement ni ne dépend de la disponibilité des sites sources pendant un voyage.
L'engagement des artefacts générés rend également la version reproductible. Une version livrée a un monde géographique connu. La mise à jour des sources est une modification de code intentionnelle qui peut être testée et examinée, et non un changement invisible côté serveur qui modifie la carte de quelqu'un du jour au lendemain.
Une base de données SQLite délibérément simple.
Le premier Gazetteer contenait 252 pays, 3,861 régions, 33,744 villes et 4,564 aéroports, soit environ 14 Mo. Il utilisait la bibliothèque SQLite déjà fournie par le système d'exploitation et une petite couche locale plutôt qu'un framework de base de données plus important.
Le schéma est intentionnellement direct. Les continents contiennent des pays. Les pays contiennent des régions. Les régions contiennent des villes. Les aéroports ont des codes de pays et des coordonnées. Les identificateurs de source stables deviennent les identificateurs stockés avec une marque Mapsake : ISO alpha-2 pour un pays, un code d'administrateur GeoNames pour une région, un ID GeoNames pour une ville et un code IATA pour un aéroport.
Mapsake normalise également le nom lisible par l'homme et l'ascendance dans chaque marque enregistrée. Cette duplication est utile. Un enregistrement de voyage personnel doit rester lisible si un gazetteer ultérieur renomme un lieu, supprime un enregistrement ou n'est pas disponible lors d'une exportation. L'identifiant sert à la correspondance ; l'instantané conserve les données de l'utilisateur de manière autonome.
SQLite convient bien à cette charge de travail car la base de données est générée une fois et interrogée de nombreuses fois. Elle prend en charge les index, les transactions pendant la construction et la recherche en texte intégral sans processus de service. L'application ouvre le fichier inclus en lecture seule, il n'y a donc pas de risque de migration pour les données de référence et il est impossible qu'une écriture interrompue la corrompe.
La recherche est plus que 'contient(texte)'
Le marquage manuel commence avec un seul champ de recherche qui couvre les continents, les pays, les régions, les villes et les aéroports. Une recherche pour "san" doit trouver des villes pertinentes avant des entrées obscures ; "LAX" doit trouver l'aéroport ; et un nom saisi sans ses accents doit toujours fonctionner.
Le script de construction crée une table FTS5 avec des noms d'affichage, des noms alternatifs sélectionnés, des codes, une ascendance, des coordonnées, un type et une valeur d'importance. Le tokenizer Unicode supprime les diacritiques pour la correspondance. Au moment de la requête, Mapsake normalise les majuscules et les minuscules et les diacritiques, supprime les caractères qui pourraient devenir une syntaxe de texte intégral, ajoute une correspondance de préfixe à chaque jeton et classe les noms exacts avant les préfixes et les correspondances générales.
L’importance départage les résultats encore à égalité. Les continents et les pays ne doivent pas disparaître sous des villages de noms similaires. La population d'une ville donne un poids approprié aux lieux importants. Les grands aéroports sont classés avant les petits lorsque la correspondance de texte est comparable.
Les noms alternatifs sont intentionnellement limités. GeoNames peut fournir une liste très longue et multilingue pour un lieu populaire. Copier chaque orthographe dans l'index de l'appareil ajouterait du bruit et de la taille. Le constructeur conserve un ensemble limité et dédupliqué de variantes utiles et conserve le nom d'affichage d'origine séparément de son formulaire de recherche plié.
Les résultats de la recherche affichent le même modèle GazetteerPlace utilisé pour la navigation hiérarchique. Un utilisateur peut rechercher directement ou parcourir le continent, le pays, la région et la ville sans créer deux systèmes géographiques qui pourraient être différents.
Les lignes répondent à une question différente de celle posée par les points.
Le premier résolveur de photos a choisi la ville la plus proche et a hérité du pays et de la région de cette ville. Dans les zones densément peuplées, cela semble souvent parfait. Près d'une frontière, cela peut être incorrect d'une manière difficile à remarquer.
Imaginez une photo prise juste à l'intérieur du Montana alors que l'endroit habité le plus proche de la base de données se trouve de l'autre côté de la frontière, dans le Dakota du Nord. Le calcul de la ville la plus proche se comporte correctement, mais le résultat n'est pas le lieu administratif où la photo a été prise. Le même problème se produit aux frontières internationales, près des enclaves et au-dessus de l'eau où une côte peu peuplée n'a pas de point habité à proximité.
La géométrie de la limite répond à la question de savoir si elle contient plutôt qu'à la proximité. Mapsake décode les polygones de pays et d'administrations de Natural Earth-1, vérifie quels anneaux contiennent la coordonnée et utilise ce résultat pour protéger l'attribution du pays et de la région. Il peut ensuite rechercher la ville la plus proche contrainte au pays ou à la région du polygone.
Cela crée une division du travail utile :
- Le confinement par polygones établit la zone administrative.
- La hiérarchie du gazetteer fournit des ID et des noms stables.
- Une recherche de ville la plus proche, limitée, fournit une localisation utile sans franchir la limite qui vient d'être établie.
- Une vérification de l'aéroport le plus proche ajoute un aéroport uniquement dans une distance délibérément limitée.
Ni les données de la ligne ni le répertoire des lieux ne sont suffisants à eux seuls. Ensemble, ils transforment une coordonnée en une chaîne vérifiable.
L'audit admin-1 a révélé des incohérences systémiques.
Les polygones de pays et les polygones de régions provenaient de différentes couches de Natural Earth, et les identifiants de la couche de régions ne correspondaient pas toujours à GeoNames. Certains problèmes étaient évidents, tandis que d'autres produisaient des résultats plausibles mais incorrects.
Lors d'une conversion initiale, la géométrie du Nouveau-Brunswick a été attribuée au Québec. D'autres fonctionnalités n'avaient pas de code, contenaient un code provenant d'un système voisin ou représentaient une unité administrative différemment de l'annuaire. Un aperçu visuel de la carte du monde ne pouvait pas fiablement trouver toutes ces erreurs.
Le mécanisme de remplacement traite les villes comme une source d'oracle. Pour chaque polygone candidat, il demande à quelle région du Gazetteer appartiennent les villes qui se trouvent réellement à l'intérieur de celui-ci. Les codes Natural Earth existants sont validés plutôt que d'être utilisés aveuglément. Les éléments qui ne passent pas la validation peuvent être réaffectés spatialement, divisés ou exclus. La sortie générée utilise ensuite les ID de région exacts qui sont déjà présents dans SQLite.
Il s'agit d'une forme pratique de test inter-ensembles de données. Un polygone qui prétend être une région doit contenir un échantillon convaincant de villes qui prétendent être la même région. Lorsque les deux sources ne sont pas d'accord, le système génère une preuve au lieu de choisir silencieusement la valeur qui a été chargée en premier.
La même vérification a révélé des cas particuliers, tels que des dépendances habitées intégrées à la géométrie d'un pays. Mapsake corrige un petit nombre de ces cas afin qu'une photo puisse être associée au même pays que celui que comprennent les paramètres du Gazetteer et du compteur de pays.
Lignes plus nettes sans envoyer le monde en taille originale.
La première version utilisait la couche de pays de 1:110 millions de Natural Earth. Elle était compacte et rapide, mais les côtes sont devenues visiblement grossières lorsque Mapsake a ajouté des cartes plus détaillées, des histoires de lieux et des vues régionales.
La carte a ensuite été déplacée vers la couche 1:10 millions. Cette source est beaucoup plus détaillée, donc l'inclure et la rendre sans modification aurait augmenté l'espace de stockage, le temps de décodage, la construction de la superposition et le travail de recoloration. Le pipeline de construction simplifie chaque anneau à l'aide de Douglas-Peucker avec une tolérance d'environ 0.004 degrés, puis arrondit les coordonnées à une précision stable.
Le fichier de pays résultant fait environ 6.5 Mo. Il conserve les détails utiles du littoral aux niveaux de zoom que Mapsake affiche, tout en supprimant les sommets qui se situeraient sur les mêmes pixels. La géométrie Admin-1 subit un processus de validation et de simplification similaire.
La simplification a une contrainte de correction : un polygone plus petit doit toujours prendre les mêmes décisions pour les coordonnées réelles des photos. Les configurations de référence ultérieures placent délibérément des points autour des bordures et comparent la détection d'impact optimisée à une référence figée. Les lignes plus rapides ne sont utiles que s'elles répondent toujours à la même question de confinement.
Extension de 34,000 à 234,000 villes.
La première base de données utilisait les villes GeoNames avec une population supérieure à 15,000. Cela a permis de maintenir le package compact, mais cela a laissé les voyages en zone rurale, les petites îles, les villes de randonnée et de nombreux lieux de résidence avec une étiquette inutilement éloignée.
Mapsake a ensuite adopté GeoNames. villes500, couvrant les lieux habités avec une population d'environ 500 habitants ou plus, ainsi que les sièges administratifs. Le tableau des villes a augmenté d'environ sept fois, pour atteindre environ 234,000 enregistrements, et la base de données incluse est passée d'environ 14 Mo à environ 69 Mo.
Cet échange a été effectué après la suppression de l'App Clip. La limite de téléchargement du Clip était la principale raison de la restriction de la base de données. Une fois que l'application principale était le seul consommateur, une meilleure couverture était plus précieuse que de maintenir une limite artificielle pour les petites villes.
L'index de texte intégral reste sélectif quant aux noms alternatifs des lieux mineurs, et la navigation par région limite toujours ce qui est rendu à la fois. Les données peuvent être larges sans nécessiter que chaque écran matérialise toute la table.
La couverture est la plus importante là où un géocodificateur réseau serait le moins fiable. Une petite ville lors d'un voyage à l'isolement ne doit pas être étiquetée comme une ville située à des heures de là simplement parce que l'ensemble de données compact l'a omise.
La recherche de la ville la plus proche est devenue un problème de performance partagé.
La requête originale de la ville la plus proche élargissait une boîte de latitude et de longitude, demandait à SQLite de calculer une distance pondérée pour chaque candidat, créait un tri temporaire et renvoyait la ligne la plus proche. Il était facile à comprendre et suffisamment précis, mais une bibliothèque de photos 82,000 a transformé un faible coût par requête en quelques secondes de travail répétitif lors des importations, des souvenirs, de la dérivation de la carte et de Constellations.
La première amélioration a ajouté une recherche de test d'une largeur de 0.4 degré avant d'utiliser des options plus larges. Les emplacements denses trouvaient généralement une ville dans un ensemble de candidats beaucoup plus petit. La mémoïsation et un cache persistant de cellules géographiques ont également empêché les photos à proximité de répéter le même travail.
L'amélioration la plus importante charge les coordonnées numériques des villes 234,000 dans un index en mémoire trié par latitude. Une recherche binaire trouve la tranche à l'intérieur de la fenêtre de latitude actuelle, une boucle limitée vérifie la longitude et la distance pondérée, et seule la ligne gagnante est extraite de SQLite. Un petit cache d'enregistrements maintient les gagnants répétés à faible coût.
Le comportement d'ajustement automatique n'a pas changé. La fonction optimisée choisit toujours la ville la plus proche dans la première zone de recherche non vide, y compris une résolution déterministe des égalités. Une version de référence de l'ancien code SQL vérifie des centaines de coordonnées proches des frontières pour une équivalence d'ID exacte.
Sur le simulateur, le temps moyen pour la dérivation de la ville la plus proche est passé de 2.11 secondes à 5.2 millisecondes. Sur un iPhone physique, le temps est passé de 3.06 secondes à 6.35 millisecondes. Ces gains ont permis plusieurs fonctionnalités car le répertoire géographique est une infrastructure partagée plutôt qu'un détail de mise en œuvre privé de l'écran d'importation.
Des polygones superposés ont révélé un bug de correction.
Le mécanisme de vérification des performances a révélé un bug qui existait avant l'optimisation. Certains polygones admin-1 se chevauchent intentionnellement, en particulier les régions des capitales situées dans une région environnante. Berlin et Brandebourg, Séoul et Gyeonggi, et la ville de Kiev et son oblast en sont des exemples.
Le test de collision d'origine acceptait le polygone correspondant qui apparaissait en premier dans un dictionnaire Swift. L'ordre d'itération d'un dictionnaire change entre les processus, de sorte que la même coordonnée pourrait acquérir une région différente après le redémarrage de l'application.
Mapsake trie désormais les candidats superposés par aire de polygone et permet à la caractéristique la plus petite et la plus spécifique de gagner. L'implémentation de référence suit la même règle. Une photo de référence 82,000 doit produire zéro différence entre les chemins de dérivation froids et chauds avant qu'une optimisation de géométrie ne soit acceptée.
Ce bug est un bon rappel que "à l'intérieur d'un polygone" n'est pas toujours une question de oui ou de non. Les données géographiques contiennent des enclaves, des capitales imbriquées, des passages de l'antiméridien, des multipolygones, des trous, des frontières contestées et des conventions de source. Une politique déterministe est aussi importante que l'algorithme de point à l'intérieur du polygone.
Le mode hors ligne est une fonctionnalité de confidentialité et une fonctionnalité du produit.
L'importation de photos de Mapsake peut traiter une grande bibliothèque sans envoyer ses coordonnées à un service de géocodage tiers. Cela protège les emplacements de voyage et de domicile sensibles, élimine les prix par requête, évite la limitation du débit et rend les progrès prévisibles.
Cela rend également l'édition cohérente. La recherche manuelle, l'importation de photos, les cartes de passeport, les succès, les instantanés d'amis, la correspondance des tampons et les histoires de lieux parlent la même langue car ils utilisent les mêmes identifiants de lieu stables. Un aéroport importé à partir d'un journal de vol peut être dédupliqué avec le même aéroport détecté à proximité d'une photo. Une ville trouvée par une recherche peut correspondre à la ville utilisée par Then & Now.
Ce paquet n'est pas considéré comme parfait ou permanent. Les attributions de sources sont visibles dans l'application. Les scripts de construction sont conservés à côté du code. Les corrections connues sont explicites. Les éléments de référence protègent le comportement aux coordonnées difficiles. La mise à jour du monde géographique est un événement de publication avec des conséquences vérifiables.
Ce que je conserverais si je le reconstruisais
Les choix les plus durables n'étaient pas des algorithmes individuels. Ce sont les limites des responsabilités :
- Utilisez des points pour les noms, les identités stables, la recherche et la hiérarchie.
- Utilisez des lignes et des polygones pour le contournement.
- Générez un artefact de produit en lecture seule au lieu d'analyser quatre formats sources sur un téléphone.
- Enregistrez une capture d'écran lisible avec les données de l'utilisateur tout en conservant l'ID source pour la correspondance.
- Rendez le chemin d'accès hors ligne habituel déterministe avant de l'accélérer.
- Conservez une implémentation de référence lente suffisamment longtemps pour prouver que la sortie optimisée est équivalente.
Le gazetteer a commencé comme une fonctionnalité de recherche précoce. Il est devenu l'un des systèmes fondamentaux de Mapsake, car presque toutes les fonctionnalités plus avancées doivent finalement répondre à la même question simple : quel est cet endroit ?
Pour obtenir la bonne réponse, il faut accepter que la géographie n'est pas une seule base de données ni une requête intelligente. C'est un accord minutieux entre les noms, les points, les lignes, les règles du produit et l'itinéraire que l'utilisateur s'attend à reconnaître.