feat: géolocalisation par clé cryptographique (SS58 → Duniter base58)

Au lieu de chercher par nom (title), on résout maintenant par clé :
1. buildIdentityKeyMap() : charge toutes les identités Ğ1v2 depuis Subsquid
   avec leur ownerKeyChange → currentSS58 → genesisKey → duniterKey
2. ss58ToDuniterKey() : conversion SS58 v2 (préfixe 2 octets) → base58 Ed25519
   = _id Cesium+ (même matériau cryptographique, encodage différent)
3. resolveGeoByKeys() : query Cesium+ par ids{} → résultat exact, pas d'ambiguïté
4. Cache keyMap 10 min : 1 requête Subsquid pour ~8000 identités, pas par refresh

Résultat : les membres migrés v1→v2 avec un profil Cesium+ sont correctement
géolocalisés même si leur nom v2 diffère de leur nom v1.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
syoul
2026-03-22 17:41:26 +01:00
parent 71b457892e
commit 5fe71c1801
5 changed files with 180 additions and 35 deletions

View File

@@ -23,6 +23,7 @@ export interface GeoProfile {
}
const HitSchema = z.object({
_id: z.string(),
_source: z.object({
title: z.string().optional(),
city: z.string().optional(),
@@ -39,6 +40,47 @@ const SearchResponseSchema = z.object({
}),
});
/**
* Résout les coordonnées par clé Duniter (Cesium+ _id).
* Plus fiable que par nom car la clé est unique et indépendante de la casse.
* Fonctionne pour les membres migrés v1→v2 (clé genesis = _id Cesium+).
*/
export async function resolveGeoByKeys(
duniterKeys: string[]
): Promise<Map<string, GeoProfile>> {
const unique = [...new Set(duniterKeys.filter(Boolean))];
if (unique.length === 0) return new Map();
const query = {
size: unique.length,
query: { ids: { values: unique } },
_source: ['title', 'city', 'geoPoint'],
};
const response = await fetch(`${CESIUM_ENDPOINT}/user/profile/_search`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(query),
});
if (!response.ok) throw new Error(`Cesium+ HTTP ${response.status}`);
const raw = await response.json();
const parsed = SearchResponseSchema.parse(raw);
const result = new Map<string, GeoProfile>();
for (const hit of parsed.hits.hits) {
const src = hit._source;
if (!src.geoPoint) continue;
result.set(hit._id, {
name: src.title ?? '',
city: src.city ?? 'Inconnue',
lat: src.geoPoint.lat,
lng: src.geoPoint.lon,
});
}
return result;
}
/**
* Résout les coordonnées de plusieurs membres Ğ1 par leur nom d'identité.
* Envoie une requête Elasticsearch multi-terms en un seul appel.