@@ -71,6 +71,12 @@ class _Candidate:
7171 signal_score : int
7272
7373
74+ @dataclass (frozen = True , slots = True )
75+ class _PlaceDedupeKeys :
76+ primary : set [str ]
77+ aliases : set [str ]
78+
79+
7480class ParseError (RuntimeError ):
7581 """Raised when a saved list cannot be parsed from the supplied artifacts."""
7682
@@ -388,7 +394,8 @@ def _parse_list_owner(node: JSONValue | None) -> ListOwner | None:
388394
389395def _extract_places (node : JSONValue ) -> list [Place ]:
390396 places : list [Place ] = []
391- seen : set [str ] = set ()
397+ primary_key_indexes : dict [str , int ] = {}
398+ alias_key_indexes : dict [str , int ] = {}
392399
393400 for current , ancestors in _walk_json (node ):
394401 if not _is_coordinate_tuple (current ):
@@ -405,6 +412,7 @@ def _extract_places(node: JSONValue) -> list[Place]:
405412 metadata_node = _find_place_metadata (ancestors )
406413 address = _extract_address (metadata_node )
407414 cid = _find_cid (metadata_node )
415+ cid_aliases = _find_cid_aliases (metadata_node )
408416 google_id = _find_google_id (metadata_node )
409417 name = _find_place_name (ancestors , address = address , place_record = place_record )
410418 note = _find_place_note (place_record , name = name , address = address )
@@ -422,23 +430,95 @@ def _extract_places(node: JSONValue) -> list[Place]:
422430 lng = lng ,
423431 ),
424432 cid = cid ,
433+ cid_aliases = cid_aliases ,
425434 google_id = google_id ,
426435 is_favorite = is_favorite ,
427436 added_by = _find_place_added_by (place_record ),
428437 )
429- dedupe_key = (
430- google_id
431- or (f"{ cid } :{ lat :.6f} :{ lng :.6f} " if cid is not None else None )
432- or f"{ place .name } :{ lat :.6f} :{ lng :.6f} "
438+ dedupe_keys = _place_dedupe_keys (place )
439+ duplicate_index = _duplicate_place_index (
440+ dedupe_keys ,
441+ primary_key_indexes = primary_key_indexes ,
442+ alias_key_indexes = alias_key_indexes ,
433443 )
434- if dedupe_key in seen :
444+ if duplicate_index is not None :
445+ if _place_dedupe_quality (place ) > _place_dedupe_quality (places [duplicate_index ]):
446+ places [duplicate_index ] = place
447+ _record_place_dedupe_keys (
448+ duplicate_index ,
449+ dedupe_keys ,
450+ primary_key_indexes = primary_key_indexes ,
451+ alias_key_indexes = alias_key_indexes ,
452+ )
435453 continue
436- seen .add (dedupe_key )
437454 places .append (place )
455+ _record_place_dedupe_keys (
456+ len (places ) - 1 ,
457+ dedupe_keys ,
458+ primary_key_indexes = primary_key_indexes ,
459+ alias_key_indexes = alias_key_indexes ,
460+ )
438461
439462 return places
440463
441464
465+ def _place_dedupe_keys (place : Place ) -> _PlaceDedupeKeys :
466+ primary : set [str ] = set ()
467+ aliases : set [str ] = set ()
468+ if place .google_id :
469+ primary .add (f"gid:{ place .google_id } " )
470+ if place .cid is not None :
471+ cid_suffix = f"{ place .lat :.6f} :{ place .lng :.6f} "
472+ primary .add (f"cid:{ place .cid } :{ cid_suffix } " )
473+ aliases .update (
474+ f"cid:{ cid_alias } :{ cid_suffix } "
475+ for cid_alias in place .cid_aliases
476+ )
477+ if not primary :
478+ primary .add (f"name:{ place .name } :{ place .lat :.6f} :{ place .lng :.6f} " )
479+ return _PlaceDedupeKeys (primary = primary , aliases = aliases )
480+
481+
482+ def _duplicate_place_index (
483+ keys : _PlaceDedupeKeys ,
484+ * ,
485+ primary_key_indexes : dict [str , int ],
486+ alias_key_indexes : dict [str , int ],
487+ ) -> int | None :
488+ for key in keys .primary :
489+ if key in primary_key_indexes :
490+ return primary_key_indexes [key ]
491+ for key in keys .aliases :
492+ if key in primary_key_indexes :
493+ return primary_key_indexes [key ]
494+ for key in keys .primary :
495+ if key in alias_key_indexes :
496+ return alias_key_indexes [key ]
497+ return None
498+
499+
500+ def _record_place_dedupe_keys (
501+ index : int ,
502+ keys : _PlaceDedupeKeys ,
503+ * ,
504+ primary_key_indexes : dict [str , int ],
505+ alias_key_indexes : dict [str , int ],
506+ ) -> None :
507+ for key in keys .primary :
508+ primary_key_indexes [key ] = index
509+ for key in keys .aliases :
510+ alias_key_indexes [key ] = index
511+
512+
513+ def _place_dedupe_quality (place : Place ) -> tuple [bool , bool , bool , bool ]:
514+ return (
515+ place .google_id is not None ,
516+ bool (place .cid_aliases ),
517+ place .cid is not None ,
518+ place .address is not None ,
519+ )
520+
521+
442522def _find_place_metadata (ancestors : Sequence [JSONValue ]) -> list [JSONValue ] | None :
443523 place_record = _find_place_record (ancestors )
444524 metadata_node = _place_metadata_from_record (place_record )
@@ -597,6 +677,19 @@ def _find_cid(node: list[JSONValue] | None) -> str | None:
597677 return None
598678
599679
680+ def _find_cid_aliases (node : list [JSONValue ] | None ) -> list [str ]:
681+ if node is None :
682+ return []
683+ structured_value = _safe_index (node , 6 )
684+ structured_cid = _find_cid_in_structured_slot (structured_value )
685+ if structured_cid is None :
686+ return []
687+ return _find_cid_aliases_in_structured_slot (
688+ structured_value ,
689+ selected_cid = structured_cid ,
690+ )
691+
692+
600693def _find_google_id (node : list [JSONValue ] | None ) -> str | None :
601694 if node is None :
602695 return None
@@ -660,6 +753,27 @@ def _find_cid_in_structured_slot(value: JSONValue | None) -> str | None:
660753 return _normalize_cid_token (numeric_texts [0 ])
661754
662755
756+ def _find_cid_aliases_in_structured_slot (
757+ value : JSONValue | None ,
758+ * ,
759+ selected_cid : str ,
760+ ) -> list [str ]:
761+ if not isinstance (value , list ):
762+ return []
763+ owner = _parse_list_owner (value )
764+ if owner is not None and (owner .photo_url is not None or owner .profile_id is not None ):
765+ return []
766+ aliases : list [str ] = []
767+ for item in value :
768+ text = _clean_text (item )
769+ if text is None or _LONG_INTEGER_PATTERN .fullmatch (text ) is None :
770+ continue
771+ alias = _normalize_cid_token (text )
772+ if alias is not None and alias != selected_cid and alias not in aliases :
773+ aliases .append (alias )
774+ return aliases
775+
776+
663777def _find_cid_in_fallback_value (value : JSONValue | None ) -> str | None :
664778 if isinstance (value , int | str ):
665779 return _normalize_fallback_cid_token (value )
0 commit comments