feat(places): record which search result was picked, on this instance only

The corpus behind "is our own index good enough to replace Nominatim".
When somebody searches and then picks a result, the client posts the query,
the rank and the pick. Nothing else: no user, no trip, no session, and
coordinates only to three decimals.

Deliberately decoupled from the search endpoint. Correlating a response
with a later pick would mean handing out a ticket on POST /api/maps/search
and threading it through all fifteen call sites; the client already holds
everything the row needs at the moment of the pick.

Evaluation happens offline afterwards: export the corpus, run it against a
candidate index, count how often the pick came back in the top five. That
is why collection can start before such an index exists, which is the whole
point, because the corpus needs weeks of real use and the index needs a
server, and those two waits should not be serial.

Off unless an admin switches it on, and it reads === 'true' rather than
!== 'false'. The three older places switches fail open because a migration
backfilled a row for installs already using them; nothing uses this one, so
an instance that upgrades and never visits the admin panel logs nothing.

Reading is admin only, retention is 180 days, and the switch state is on the
unauthenticated app-config endpoint on purpose: whether an instance records
what its users search for is something a visitor may know before logging in.
This commit is contained in:
Maurice
2026-09-05 23:15:51 +02:00
committed by Maurice
parent 0a2fa32fc1
commit b3d69aa760
49 changed files with 1385 additions and 16 deletions
+3
View File
@@ -195,6 +195,9 @@ const admin: TranslationStrings = {
'admin.transitProvider.googleHint': 'يستخدم مفتاح Google أعلاه للمناطق التي لا تتوفر لها بيانات في Transitous. يُحاسب على كل بحث — ويُستخدم Transitous ما دام لا يوجد مفتاح.',
'admin.transitProvider.noKeyWarning': 'تم اختيار Google، لكن لا يوجد مفتاح Google مُهيأ — لا يزال بحث النقل يستخدم Transitous. أضف مفتاحًا ضمن مفاتيح API أعلاه.',
'admin.transitProvider.personalKeyWarning': 'مفتاح Google الخاص بك وحده مضبوط، لذا يعود بحث النقل لبقية الأعضاء إلى Transitous. احفظ المفتاح أعلاه كمسؤول لتطبيقه على مستوى المثيل.',
'admin.placeShadow.title': 'سجل البحث عن الأماكن',
'admin.placeShadow.subtitle':
'تسجيل نتيجة البحث التي جرى اختيارها، حتى يمكن لاحقًا قياس فهرس أماكن آخر على عمليات بحث حقيقية. لا يغادر أي شيء هذا الخادم، ويمكن للمشرف تصدير السجل أو حذفه في أي وقت.',
'admin.bagTracking.title': 'تتبع الأمتعة',
'admin.bagTracking.subtitle': 'تفعيل الوزن وتعيين الأمتعة للعناصر',
'admin.collab.chat.title': 'الدردشة',
+3
View File
@@ -174,6 +174,9 @@ const admin: TranslationStrings = {
'admin.transitProvider.googleHint': 'Usa a chave do Google acima, para regiões sem dados no Transitous. Cobrado por busca — o Transitous é usado enquanto nenhuma chave estiver configurada.',
'admin.transitProvider.noKeyWarning': 'O Google está selecionado, mas nenhuma chave do Google está configurada — a busca de transporte ainda usa o Transitous. Adicione uma chave em Chaves de API acima.',
'admin.transitProvider.personalKeyWarning': 'Apenas a sua própria chave do Google está definida, então a busca dos outros membros ainda cai no Transitous. Salve a chave acima como admin para aplicá-la a toda a instância.',
'admin.placeShadow.title': 'Registo de pesquisas de locais',
'admin.placeShadow.subtitle':
'Registar qual resultado de pesquisa foi escolhido, para que outro índice de locais possa ser avaliado depois com pesquisas reais. Nada sai desta instância e um administrador pode exportar ou apagar o registo a qualquer momento.',
'admin.bagTracking.title': 'Rastreamento de malas',
'admin.bagTracking.subtitle': 'Ativar peso e atribuição de mala para itens da lista',
'admin.collab.chat.title': 'Chat',
+3
View File
@@ -178,6 +178,9 @@ const admin: TranslationStrings = {
'admin.transitProvider.googleHint': 'Utilitza la clau de Google anterior, per a regions sense dades a Transitous. Es factura per cerca: mentre no hi hagi clau s\'utilitza Transitous.',
'admin.transitProvider.noKeyWarning': 'Google està seleccionat, però no hi ha cap clau de Google configurada: la cerca de transport encara utilitza Transitous. Afegeix una clau a Claus d\'API a dalt.',
'admin.transitProvider.personalKeyWarning': 'Només hi ha la teva pròpia clau de Google, de manera que la cerca dels altres membres continua recorrent a Transitous. Desa la clau a dalt com a administrador per aplicar-la a tota la instància.',
'admin.placeShadow.title': 'Registre de cerques de llocs',
'admin.placeShadow.subtitle':
'Desar quin resultat de cerca s\'ha triat, per poder avaluar més endavant un altre índex de llocs amb cerques reals. No surt res d\'aquesta instància i un administrador pot exportar o esborrar el registre quan vulgui.',
'admin.bagTracking.title': "Seguiment d'equipatge",
'admin.bagTracking.subtitle': "Activar pes i assignació d'equipatge per a articles de la llista",
'admin.collab.chat.title': 'Xat',
+3
View File
@@ -175,6 +175,9 @@ const admin: TranslationStrings = {
'admin.transitProvider.googleHint': 'Použije výše uvedený klíč Google pro regiony, pro které Transitous nemá data. Účtuje se za každé hledání – dokud není klíč nastaven, používá se Transitous.',
'admin.transitProvider.noKeyWarning': 'Je vybrán Google, ale není nastaven žádný klíč Google – vyhledávání dopravy stále používá Transitous. Přidejte klíč v sekci API klíče výše.',
'admin.transitProvider.personalKeyWarning': 'Je nastaven jen váš vlastní klíč Google, takže ostatním členům se vyhledávání stále vrací k Transitous. Uložte klíč výše jako správce, aby platil pro celou instanci.',
'admin.placeShadow.title': 'Záznam vyhledávání míst',
'admin.placeShadow.subtitle':
'Zaznamenávat, který výsledek vyhledávání byl vybrán, aby bylo možné později porovnat jiný index míst na skutečných dotazech. Nic neopouští tuto instanci a správce může záznam kdykoli exportovat nebo smazat.',
'admin.bagTracking.title': 'Sledování zavazadel',
'admin.bagTracking.subtitle': 'Povolit váhu a přiřazení k zavazadlům u položek balení',
'admin.collab.chat.title': 'Chat',
+3
View File
@@ -177,6 +177,9 @@ const admin: TranslationStrings = {
'admin.transitProvider.googleHint': 'Nutzt den obigen Google-Schlüssel, für Regionen ohne Transitous-Daten. Wird pro Suche abgerechnet – solange kein Schlüssel gesetzt ist, wird Transitous verwendet.',
'admin.transitProvider.noKeyWarning': 'Google ist ausgewählt, aber es ist kein Google-Schlüssel hinterlegt — die ÖPNV-Suche nutzt weiterhin Transitous. Trage oben unter API-Schlüssel einen Schlüssel ein.',
'admin.transitProvider.personalKeyWarning': 'Es ist nur dein eigener Google-Schlüssel gesetzt, daher fällt die Suche der anderen Mitglieder weiterhin auf Transitous zurück. Speichere den Schlüssel oben als Admin, damit er instanzweit gilt.',
'admin.placeShadow.title': 'Ortssuche protokollieren',
'admin.placeShadow.subtitle':
'Festhalten, welches Suchergebnis ausgewählt wurde, damit sich ein anderer Ortsindex später an echten Suchen messen lässt. Nichts verlässt diese Instanz, und ein Administrator kann das Protokoll jederzeit exportieren oder löschen.',
'admin.bagTracking.title': 'Gepäck-Tracking',
'admin.bagTracking.subtitle': 'Gewicht und Gepäckstück-Zuordnung für Packlisteneinträge aktivieren',
'admin.collab.chat.title': 'Chat',
+3
View File
@@ -216,6 +216,9 @@ const admin: TranslationStrings = {
'admin.transitProvider.googleHint': 'Uses the Google API key above, for regions Transitous has no data for. Billed per search — Transitous is used while no key is set.',
'admin.transitProvider.noKeyWarning': 'Google is selected, but no Google API key is configured — transit search is still using Transitous. Add a key under API Keys above.',
'admin.transitProvider.personalKeyWarning': 'Only your own Google key is set, so other members\' transit searches still fall back to Transitous. Save the key above as an admin to apply it instance-wide.',
'admin.placeShadow.title': 'Place Search Log',
'admin.placeShadow.subtitle':
'Record which search result was picked, so a different place index can be measured against real searches later. Nothing leaves this instance, and an admin can export or delete the log at any time.',
'admin.bagTracking.title': 'Bag Tracking',
'admin.bagTracking.subtitle': 'Enable weight and bag assignment for packing items',
'admin.collab.chat.title': 'Chat',
+3
View File
@@ -176,6 +176,9 @@ const admin: TranslationStrings = {
'admin.transitProvider.googleHint': 'Usa la clave de Google anterior, para regiones sin datos en Transitous. Se cobra por búsqueda: mientras no haya clave se usa Transitous.',
'admin.transitProvider.noKeyWarning': 'Google está seleccionado, pero no hay ninguna clave de Google configurada: la búsqueda de transporte sigue usando Transitous. Añade una clave en Claves de API arriba.',
'admin.transitProvider.personalKeyWarning': 'Solo está configurada tu propia clave de Google, así que la búsqueda de los demás miembros sigue recurriendo a Transitous. Guarda la clave arriba como administrador para aplicarla a toda la instancia.',
'admin.placeShadow.title': 'Registro de búsquedas de lugares',
'admin.placeShadow.subtitle':
'Guardar qué resultado de búsqueda se eligió, para poder evaluar más adelante otro índice de lugares con búsquedas reales. Nada sale de esta instancia y un administrador puede exportar o borrar el registro cuando quiera.',
'admin.bagTracking.title': 'Seguimiento de equipaje',
'admin.bagTracking.subtitle': 'Activar peso y asignación de equipaje para artículos de la lista',
'admin.collab.chat.title': 'Chat',
+3
View File
@@ -178,6 +178,9 @@ const admin: TranslationStrings = {
'admin.transitProvider.googleHint': 'Utilise la clé Google ci-dessus, pour les régions sans données Transitous. Facturé à la recherche — tant qu\'aucune clé n\'est définie, Transitous est utilisé.',
'admin.transitProvider.noKeyWarning': 'Google est sélectionné, mais aucune clé Google n\'est configurée — la recherche de transports utilise toujours Transitous. Ajoutez une clé dans Clés d\'API ci-dessus.',
'admin.transitProvider.personalKeyWarning': 'Seule votre propre clé Google est définie : la recherche des autres membres continue de basculer sur Transitous. Enregistrez la clé ci-dessus en tant qu\'administrateur pour l\'appliquer à toute l\'instance.',
'admin.placeShadow.title': 'Journal des recherches de lieux',
'admin.placeShadow.subtitle':
'Enregistrer quel résultat de recherche a été retenu, afin de pouvoir évaluer plus tard un autre index de lieux sur de vraies recherches. Rien ne quitte cette instance, et un administrateur peut exporter ou supprimer le journal à tout moment.',
'admin.bagTracking.title': 'Suivi des bagages',
'admin.bagTracking.subtitle': "Activer le poids et l'attribution de bagages pour les articles",
'admin.collab.chat.title': 'Chat',
+3
View File
@@ -223,6 +223,9 @@ const admin: TranslationStrings = {
'admin.transitProvider.googleHint': 'Χρησιμοποιεί το παραπάνω κλειδί Google, για περιοχές χωρίς δεδομένα στο Transitous. Χρεώνεται ανά αναζήτηση — όσο δεν υπάρχει κλειδί, χρησιμοποιείται το Transitous.',
'admin.transitProvider.noKeyWarning': 'Έχει επιλεγεί το Google, αλλά δεν έχει ρυθμιστεί κλειδί Google — η αναζήτηση μέσων μεταφοράς εξακολουθεί να χρησιμοποιεί το Transitous. Προσθέστε κλειδί στα Κλειδιά API παραπάνω.',
'admin.transitProvider.personalKeyWarning': 'Έχει οριστεί μόνο το δικό σας κλειδί Google, οπότε η αναζήτηση των υπόλοιπων μελών επιστρέφει στο Transitous. Αποθηκεύστε το κλειδί παραπάνω ως διαχειριστής για να ισχύει σε όλη την εγκατάσταση.',
'admin.placeShadow.title': 'Αρχείο αναζητήσεων τόπων',
'admin.placeShadow.subtitle':
'Καταγραφή του ποιο αποτέλεσμα αναζήτησης επιλέχθηκε, ώστε ένα διαφορετικό ευρετήριο τόπων να μετρηθεί αργότερα με πραγματικές αναζητήσεις. Τίποτα δεν φεύγει από αυτήν την εγκατάσταση και ένας διαχειριστής μπορεί ανά πάσα στιγμή να εξαγάγει ή να διαγράψει το αρχείο.',
'admin.bagTracking.title': 'Παρακολούθηση Αποσκευών',
'admin.bagTracking.subtitle': 'Ενεργοποίηση βάρους και ανάθεσης τσάντας για αντικείμενα πακεταρίσματος',
'admin.collab.chat.title': 'Συνομιλία',
+3
View File
@@ -177,6 +177,9 @@ const admin: TranslationStrings = {
'admin.transitProvider.googleHint': 'A fenti Google-kulcsot használja azokhoz a régiókhoz, amelyekhez a Transitousnak nincs adata. Keresésenként számlázódik – amíg nincs kulcs, a Transitous fut.',
'admin.transitProvider.noKeyWarning': 'A Google van kiválasztva, de nincs beállítva Google-kulcs – a közlekedési keresés továbbra is a Transitous-t használja. Adj hozzá kulcsot a fenti API-kulcsoknál.',
'admin.transitProvider.personalKeyWarning': 'Csak a saját Google-kulcsod van beállítva, így a többi tag keresése továbbra is a Transitous-ra esik vissza. Mentsd a kulcsot fent adminként, hogy az egész példányra érvényes legyen.',
'admin.placeShadow.title': 'Helykeresési napló',
'admin.placeShadow.subtitle':
'Rögzíti, melyik találatot választották ki, hogy egy másik helyindexet később valódi kereséseken lehessen mérni. Semmi nem hagyja el ezt a példányt, és egy rendszergazda bármikor exportálhatja vagy törölheti a naplót.',
'admin.bagTracking.title': 'Poggyászkövetés',
'admin.bagTracking.subtitle': 'Súly- és táskahozzárendelés engedélyezése csomagolási tételeknél',
'admin.collab.chat.title': 'Chat',
+3
View File
@@ -219,6 +219,9 @@ const admin: TranslationStrings = {
'admin.transitProvider.googleHint': 'Menggunakan kunci Google di atas, untuk wilayah yang tidak memiliki data Transitous. Ditagih per pencarian — selama belum ada kunci, Transitous yang dipakai.',
'admin.transitProvider.noKeyWarning': 'Google dipilih, tetapi belum ada kunci Google yang dikonfigurasi — pencarian transportasi masih memakai Transitous. Tambahkan kunci di Kunci API di atas.',
'admin.transitProvider.personalKeyWarning': 'Hanya kunci Google milikmu yang disetel, sehingga pencarian anggota lain tetap kembali ke Transitous. Simpan kunci di atas sebagai admin agar berlaku untuk seluruh instance.',
'admin.placeShadow.title': 'Log pencarian tempat',
'admin.placeShadow.subtitle':
'Mencatat hasil pencarian mana yang dipilih, agar indeks tempat lain nanti bisa diukur dengan pencarian sungguhan. Tidak ada yang meninggalkan instans ini, dan admin bisa mengekspor atau menghapus log kapan saja.',
'admin.bagTracking.title': 'Pelacak Tas',
'admin.bagTracking.subtitle': 'Aktifkan berat dan penugasan tas untuk item packing',
'admin.collab.chat.title': 'Chat',
+3
View File
@@ -176,6 +176,9 @@ const admin: TranslationStrings = {
'admin.transitProvider.googleHint': 'Usa la chiave Google qui sopra, per le regioni senza dati Transitous. Addebitato per ricerca: finché non è impostata una chiave viene usato Transitous.',
'admin.transitProvider.noKeyWarning': 'Google è selezionato, ma non è configurata alcuna chiave Google: la ricerca del trasporto usa ancora Transitous. Aggiungi una chiave in Chiavi API qui sopra.',
'admin.transitProvider.personalKeyWarning': 'È impostata solo la tua chiave Google, quindi la ricerca degli altri membri continua a ripiegare su Transitous. Salva la chiave qui sopra come amministratore per applicarla all\'intera istanza.',
'admin.placeShadow.title': 'Registro delle ricerche di luoghi',
'admin.placeShadow.subtitle':
'Annotare quale risultato di ricerca è stato scelto, così da poter valutare in seguito un altro indice di luoghi su ricerche reali. Nulla esce da questa istanza e un amministratore può esportare o cancellare il registro in qualsiasi momento.',
'admin.bagTracking.title': 'Tracciamento valigia',
'admin.bagTracking.subtitle': "Abilita il peso e l'assegnazione della valigia per gli elementi della lista valigia",
'admin.collab.chat.title': 'Chat',
+3
View File
@@ -208,6 +208,9 @@ const admin: TranslationStrings = {
'admin.transitProvider.googleHint': '上記の Google キーを使用し、Transitous にデータがない地域に対応します。検索ごとに課金されます。キーが未設定の間は Transitous を使用します。',
'admin.transitProvider.noKeyWarning': 'Google が選択されていますが、Google キーが設定されていません。公共交通の検索は引き続き Transitous を使用します。上の「API キー」でキーを追加してください。',
'admin.transitProvider.personalKeyWarning': 'あなた自身の Google キーのみが設定されているため、他のメンバーの検索は引き続き Transitous になります。インスタンス全体に適用するには、管理者として上でキーを保存してください。',
'admin.placeShadow.title': '場所検索の記録',
'admin.placeShadow.subtitle':
'どの検索結果が選ばれたかを記録し、後で別の場所インデックスを実際の検索で評価できるようにします。データがこのインスタンスの外に出ることはなく、管理者はいつでも書き出しや削除ができます。',
'admin.bagTracking.title': 'バッグ管理',
'admin.bagTracking.subtitle': '持ち物の重量とバッグ割り当てを有効化',
'admin.collab.chat.title': 'チャット',
+3
View File
@@ -214,6 +214,9 @@ const admin: TranslationStrings = {
'admin.transitProvider.googleHint': '위의 Google 키를 사용하여 Transitous에 데이터가 없는 지역을 처리합니다. 검색당 과금되며, 키가 없으면 Transitous를 사용합니다.',
'admin.transitProvider.noKeyWarning': 'Google이 선택되었지만 Google 키가 설정되지 않았습니다 — 대중교통 검색은 여전히 Transitous를 사용합니다. 위의 API 키에서 키를 추가하세요.',
'admin.transitProvider.personalKeyWarning': '본인의 Google 키만 설정되어 있어 다른 멤버의 검색은 여전히 Transitous로 대체됩니다. 인스턴스 전체에 적용하려면 관리자로 위에서 키를 저장하세요.',
'admin.placeShadow.title': '장소 검색 기록',
'admin.placeShadow.subtitle':
'어떤 검색 결과가 선택되었는지 기록하여, 나중에 다른 장소 색인을 실제 검색으로 평가할 수 있게 합니다. 이 인스턴스 밖으로 나가는 것은 없으며 관리자는 언제든지 기록을 내보내거나 삭제할 수 있습니다.',
'admin.bagTracking.title': '가방 추적',
'admin.bagTracking.subtitle': '짐 항목에 무게 및 가방 배정을 활성화합니다',
'admin.collab.chat.title': '채팅',
+3
View File
@@ -177,6 +177,9 @@ const admin: TranslationStrings = {
'admin.transitProvider.googleHint': 'Gebruikt de Google-sleutel hierboven, voor regio\'s zonder Transitous-data. Wordt per zoekopdracht afgerekend — zolang er geen sleutel is, wordt Transitous gebruikt.',
'admin.transitProvider.noKeyWarning': 'Google is geselecteerd, maar er is geen Google-sleutel ingesteld — het ov-zoeken gebruikt nog steeds Transitous. Voeg hierboven een sleutel toe bij API-sleutels.',
'admin.transitProvider.personalKeyWarning': 'Alleen je eigen Google-sleutel is ingesteld, dus het zoeken van andere leden valt nog steeds terug op Transitous. Sla de sleutel hierboven op als beheerder om hem instantiebreed toe te passen.',
'admin.placeShadow.title': 'Logboek van plaatszoekopdrachten',
'admin.placeShadow.subtitle':
'Vastleggen welk zoekresultaat is gekozen, zodat een andere plaatsindex later aan echte zoekopdrachten kan worden getoetst. Er verlaat niets deze instantie en een beheerder kan het logboek altijd exporteren of verwijderen.',
'admin.bagTracking.title': 'Bagagetracking',
'admin.bagTracking.subtitle': 'Gewicht en bagagetoewijzing inschakelen voor paklijstitems',
'admin.collab.chat.title': 'Chat',
+3
View File
@@ -170,6 +170,9 @@ const admin: TranslationStrings = {
'admin.transitProvider.googleHint': 'Używa powyższego klucza Google w regionach, dla których Transitous nie ma danych. Rozliczane za wyszukiwanie — dopóki nie ustawiono klucza, działa Transitous.',
'admin.transitProvider.noKeyWarning': 'Wybrano Google, ale nie skonfigurowano klucza Google — wyszukiwanie transportu nadal korzysta z Transitous. Dodaj klucz w sekcji Klucze API powyżej.',
'admin.transitProvider.personalKeyWarning': 'Ustawiono tylko Twój własny klucz Google, więc wyszukiwanie pozostałych członków nadal wraca do Transitous. Zapisz klucz powyżej jako administrator, aby obowiązywał w całej instancji.',
'admin.placeShadow.title': 'Dziennik wyszukiwania miejsc',
'admin.placeShadow.subtitle':
'Zapisywanie, który wynik wyszukiwania został wybrany, aby później ocenić inny indeks miejsc na prawdziwych zapytaniach. Nic nie opuszcza tej instancji, a administrator może w każdej chwili wyeksportować lub usunąć dziennik.',
'admin.bagTracking.title': 'Kontrola bagażu',
'admin.bagTracking.subtitle': 'Włącz wagę i przypisywanie do toreb dla przedmiotów do pakowania',
'admin.collab.chat.title': 'Czat',
+3
View File
@@ -177,6 +177,9 @@ const admin: TranslationStrings = {
'admin.transitProvider.googleHint': 'Использует указанный выше ключ Google для регионов, по которым у Transitous нет данных. Оплачивается за каждый поиск — пока ключ не задан, используется Transitous.',
'admin.transitProvider.noKeyWarning': 'Выбран Google, но ключ Google не настроен — поиск транспорта по-прежнему использует Transitous. Добавьте ключ в разделе «Ключи API» выше.',
'admin.transitProvider.personalKeyWarning': 'Задан только ваш собственный ключ Google, поэтому поиск других участников по-прежнему возвращается к Transitous. Сохраните ключ выше от имени администратора, чтобы он действовал для всей установки.',
'admin.placeShadow.title': 'Журнал поиска мест',
'admin.placeShadow.subtitle':
'Записывать, какой результат поиска был выбран, чтобы позже оценить другой индекс мест на реальных запросах. Ничего не покидает этот сервер, и администратор может в любой момент выгрузить или удалить журнал.',
'admin.bagTracking.title': 'Отслеживание багажа',
'admin.bagTracking.subtitle': 'Включить вес и привязку к багажу для вещей',
'admin.collab.chat.title': 'Чат',
+3
View File
@@ -220,6 +220,9 @@ const admin: TranslationStrings = {
'admin.transitProvider.googleHint': 'Använder Google-nyckeln ovan, för regioner som Transitous saknar data för. Debiteras per sökning – så länge ingen nyckel är angiven används Transitous.',
'admin.transitProvider.noKeyWarning': 'Google är valt, men ingen Google-nyckel är konfigurerad – kollektivtrafiksökningen använder fortfarande Transitous. Lägg till en nyckel under API-nycklar ovan.',
'admin.transitProvider.personalKeyWarning': 'Bara din egen Google-nyckel är angiven, så andra medlemmars sökningar faller fortfarande tillbaka på Transitous. Spara nyckeln ovan som administratör för att den ska gälla hela instansen.',
'admin.placeShadow.title': 'Logg över platssökningar',
'admin.placeShadow.subtitle':
'Registrera vilket sökresultat som valdes, så att ett annat platsindex senare kan mätas mot verkliga sökningar. Ingenting lämnar den här instansen och en administratör kan när som helst exportera eller radera loggen.',
'admin.bagTracking.title': 'Spårning av väskor',
'admin.bagTracking.subtitle': 'Aktivera vikt- och väskfördelning för packningsartiklar',
'admin.collab.chat.title': 'Chatt',
+3
View File
@@ -221,6 +221,9 @@ const admin: TranslationStrings = {
'admin.transitProvider.googleHint': 'Transitous\'un verisi olmayan bölgeler için yukarıdaki Google anahtarını kullanır. Arama başına ücretlendirilir — anahtar ayarlanmadığı sürece Transitous kullanılır.',
'admin.transitProvider.noKeyWarning': 'Google seçili, ancak yapılandırılmış bir Google anahtarı yok — toplu taşıma araması hâlâ Transitous kullanıyor. Yukarıdaki API Anahtarları bölümünden bir anahtar ekleyin.',
'admin.transitProvider.personalKeyWarning': 'Yalnızca kendi Google anahtarınız ayarlı, bu yüzden diğer üyelerin araması hâlâ Transitous\'a düşüyor. Tüm kuruluma uygulamak için anahtarı yukarıda yönetici olarak kaydedin.',
'admin.placeShadow.title': 'Yer arama günlüğü',
'admin.placeShadow.subtitle':
'Hangi arama sonucunun seçildiğini kaydeder, böylece başka bir yer dizini daha sonra gerçek aramalarla ölçülebilir. Hiçbir şey bu sunucudan çıkmaz ve bir yönetici günlüğü istediği zaman dışa aktarabilir veya silebilir.',
'admin.bagTracking.title': 'Çanta Takibi',
'admin.bagTracking.subtitle': 'Paketleme öğeleri için ağırlık ve torba atamasını etkinleştirin',
'admin.collab.chat.title': 'Sohbet',
+3
View File
@@ -177,6 +177,9 @@ const admin: TranslationStrings = {
'admin.transitProvider.googleHint': 'Використовує наведений вище ключ Google для регіонів, де Transitous не має даних. Оплата за кожен пошук — доки ключ не задано, використовується Transitous.',
'admin.transitProvider.noKeyWarning': 'Вибрано Google, але ключ Google не налаштовано — пошук транспорту досі використовує Transitous. Додайте ключ у розділі «Ключі API» вище.',
'admin.transitProvider.personalKeyWarning': 'Задано лише ваш власний ключ Google, тож пошук інших учасників усе одно повертається до Transitous. Збережіть ключ вище як адміністратор, щоб він діяв для всієї інсталяції.',
'admin.placeShadow.title': 'Журнал пошуку місць',
'admin.placeShadow.subtitle':
'Записувати, який результат пошуку було обрано, щоб згодом оцінити інший індекс місць на справжніх запитах. Ніщо не залишає цей сервер, і адміністратор будь-коли може вивантажити або видалити журнал.',
'admin.bagTracking.title': 'Відстеження багажу',
'admin.bagTracking.subtitle': 'Увімкнути вагу та прив’язку до багажу для речей',
'admin.collab.chat.title': 'Чат',
+3
View File
@@ -219,6 +219,9 @@ const admin: TranslationStrings = {
'admin.transitProvider.googleHint': 'Dùng khóa Google ở trên, cho những khu vực Transitous không có dữ liệu. Tính phí theo mỗi lượt tìm kiếm — khi chưa đặt khóa thì vẫn dùng Transitous.',
'admin.transitProvider.noKeyWarning': 'Đã chọn Google, nhưng chưa cấu hình khóa Google — tìm kiếm giao thông vẫn dùng Transitous. Thêm khóa ở mục Khóa API phía trên.',
'admin.transitProvider.personalKeyWarning': 'Chỉ khóa Google của riêng bạn được đặt, nên tìm kiếm của các thành viên khác vẫn quay về Transitous. Lưu khóa ở trên với quyền quản trị để áp dụng cho toàn bộ máy chủ.',
'admin.placeShadow.title': 'Nhật ký tìm kiếm địa điểm',
'admin.placeShadow.subtitle':
'Ghi lại kết quả tìm kiếm nào đã được chọn, để sau này có thể đánh giá một chỉ mục địa điểm khác bằng những lượt tìm kiếm thật. Không có gì rời khỏi máy chủ này, và quản trị viên có thể xuất hoặc xóa nhật ký bất cứ lúc nào.',
'admin.bagTracking.title': 'Theo dõi túi',
'admin.bagTracking.subtitle': 'Cho phép phân bổ trọng lượng và túi để đóng gói các mặt hàng',
'admin.collab.chat.title': 'Trò chuyện',
+3
View File
@@ -211,6 +211,9 @@ const admin: TranslationStrings = {
'admin.transitProvider.googleHint': '使用上方的 Google 金鑰,涵蓋 Transitous 沒有資料的地區。依每次搜尋計費——未設定金鑰時仍使用 Transitous。',
'admin.transitProvider.noKeyWarning': '已選擇 Google,但尚未設定 Google 金鑰——大眾運輸搜尋仍在使用 Transitous。請在上方的 API 金鑰中新增金鑰。',
'admin.transitProvider.personalKeyWarning': '只設定了你自己的 Google 金鑰,因此其他成員的搜尋仍會回退到 Transitous。請以管理員身分在上方儲存金鑰,使其套用至整個執行個體。',
'admin.placeShadow.title': '地點搜尋紀錄',
'admin.placeShadow.subtitle':
'記錄使用者選了哪一筆搜尋結果,以便日後用真實的搜尋來評估另一套地點索引。資料不會離開本執行個體,管理員隨時可以匯出或刪除紀錄。',
'admin.bagTracking.title': '行李追蹤',
'admin.bagTracking.subtitle': '為打包物品啟用重量和行李分配',
'admin.collab.chat.title': '聊天',
+3
View File
@@ -172,6 +172,9 @@ const admin: TranslationStrings = {
'admin.transitProvider.googleHint': '使用上方的 Google 密钥,覆盖 Transitous 没有数据的地区。按每次搜索计费——未设置密钥时仍使用 Transitous。',
'admin.transitProvider.noKeyWarning': '已选择 Google,但未配置 Google 密钥——公共交通搜索仍在使用 Transitous。请在上方的 API 密钥中添加密钥。',
'admin.transitProvider.personalKeyWarning': '只设置了你自己的 Google 密钥,因此其他成员的搜索仍会回退到 Transitous。请以管理员身份在上方保存密钥,使其对整个实例生效。',
'admin.placeShadow.title': '地点搜索记录',
'admin.placeShadow.subtitle':
'记录用户选中了哪条搜索结果,以便日后用真实的搜索来评估另一套地点索引。数据不会离开本实例,管理员随时可以导出或删除记录。',
'admin.bagTracking.title': '行李追踪',
'admin.bagTracking.subtitle': '为打包物品启用重量和行李分配',
'admin.collab.chat.title': '聊天',
+1
View File
@@ -18,6 +18,7 @@ export * from './airport/airport.schema';
export * from './config/config.schema';
export * from './system-notice/system-notice.schema';
export * from './maps/maps.schema';
export * from './maps/place-shadow.schema';
export * from './category/category.schema';
export * from './tag/tag.schema';
export * from './notification/notification.schema';
+115
View File
@@ -0,0 +1,115 @@
import { z } from 'zod';
/**
* Place shadow log — the corpus behind the "is our own index good enough"
* question, recorded on this instance and nowhere else.
*
* The measurement we actually need is narrow: when someone searched for a place
* and then picked one of the results, would a different index have offered that
* same place near the top? Answering it needs the query, the pick, and nothing
* else — so nothing else is recorded. In particular there is no user id, no
* session, no trip, and no result list: the corpus is about queries, not about
* who typed them.
*
* Deliberately decoupled from the search endpoint. Correlating a search
* response with a later pick would mean handing out a ticket on
* POST /api/maps/search and threading it through every one of the fifteen call
* sites; the client already holds everything the row needs at the moment of the
* pick, so it posts the row itself and the search path stays untouched.
*
* Evaluation happens offline and afterwards. Export the corpus, run it against
* a candidate index, count how often the pick came back in the top five. That
* is why collection can start before any such index exists, which is the whole
* point: the corpus needs weeks of real use, the index needs a server, and
* these two waits should not be serial.
*/
/** Where the live result the user picked came from, as the provider named it. */
export const placeShadowSourceSchema = z.string().min(1).max(40);
export const placeShadowPickRequestSchema = z.object({
/** What the user typed. Trimmed by the client; stored verbatim. */
query: z.string().min(1).max(200),
/** UI language at the time, because result ranking is language-dependent. */
lang: z.string().max(35).optional(),
/**
* The map centre the search was biased toward, when there was one. Rounded to
* three decimals (about 100 m) client-side: the bias only has to reproduce
* the ranking, and a coarser value is a weaker fingerprint of where someone
* was looking.
*/
biasLat: z.number().optional(),
biasLng: z.number().optional(),
/** 'nominatim', 'google', 'photon', … — whatever the search envelope said. */
source: placeShadowSourceSchema,
/** Zero-based position of the picked result in the live list. */
liveRank: z.number().int().min(0).max(500),
/** How many results the live provider returned for that query. */
liveCount: z.number().int().min(1).max(500),
/** Name of the picked place, as the provider spelled it. */
pickedName: z.string().min(1).max(300),
pickedLat: z.number().min(-90).max(90),
pickedLng: z.number().min(-180).max(180),
/**
* Provider id of the pick when there is one (`osm:node/123`, a Google place
* id). Not required: it is a convenience for matching, and coordinates plus
* name are what the evaluation actually compares on.
*/
pickedPlaceId: z.string().max(300).nullable().optional(),
});
export type PlaceShadowPickRequest = z.infer<typeof placeShadowPickRequestSchema>;
/** Answered even when the log is switched off, so the client never has to care. */
export const placeShadowPickResultSchema = z.object({
recorded: z.boolean(),
});
export type PlaceShadowPickResult = z.infer<typeof placeShadowPickResultSchema>;
export const placeShadowRowSchema = z.object({
id: z.number().int(),
createdAt: z.string(),
query: z.string(),
lang: z.string().nullable(),
biasLat: z.number().nullable(),
biasLng: z.number().nullable(),
source: z.string(),
liveRank: z.number().int(),
liveCount: z.number().int(),
pickedName: z.string(),
pickedLat: z.number(),
pickedLng: z.number(),
pickedPlaceId: z.string().nullable(),
});
export type PlaceShadowRow = z.infer<typeof placeShadowRowSchema>;
export const placeShadowExportResultSchema = z.object({
/** Bumped when the row shape changes, so an evaluator can refuse an old dump. */
version: z.literal(1),
generatedAt: z.string(),
rows: z.array(placeShadowRowSchema),
/** Present when the page was capped; pass it back as `after` for the next one. */
nextAfter: z.number().int().nullable(),
});
export type PlaceShadowExportResult = z.infer<typeof placeShadowExportResultSchema>;
export const placeShadowSummaryResultSchema = z.object({
enabled: z.boolean(),
total: z.number().int(),
oldest: z.string().nullable(),
newest: z.string().nullable(),
/** How many days a row survives before the nightly job removes it. */
retentionDays: z.number().int(),
bySource: z.array(z.object({ source: z.string(), count: z.number().int() })),
/**
* Where the picked result sat in the live list. This is the baseline the
* candidate index has to beat, and it is worth having on its own: if the live
* provider already puts the pick outside the top five most of the time, the
* users are working around the search rather than using it.
*/
liveRankBuckets: z.array(z.object({ bucket: z.string(), count: z.number().int() })),
/** Share of picks the live provider had at position 0. */
liveTopOneShare: z.number(),
/** Share of picks the live provider had within the first five. */
liveTopFiveShare: z.number(),
});
export type PlaceShadowSummaryResult = z.infer<typeof placeShadowSummaryResultSchema>;