Built for on-device autocomplete in a mobile transit app.
Two approaches are available, both suitable for Android / mobile apps.
1from huggingface_hub import hf_hub_download
2
3REPO = "Luke-Yong/sg-transit-prefix-encoder"
4bin_path = hf_hub_download(REPO, "embeddings.bin")
5names_path = hf_hub_download(REPO, "embeddings_names.json")
1from huggingface_hub import hf_hub_download
2
3REPO = "Luke-Yong/sg-transit-prefix-encoder"
4model_path = hf_hub_download(REPO, "prefix_encoder_scripted.pt")
5vocab_path = hf_hub_download(REPO, "char_vocab.json")
6csv_path = hf_hub_download(REPO, "busstops_and_stations_words_prefixes.csv")
1import json, struct
2import torch
3import numpy as np
4from char_encoder import CharEncoder, CharTokenizer
5from huggingface_hub import hf_hub_download
6
7REPO = "Luke-Yong/sg-transit-prefix-encoder"
8
9# Load model and tokenizer from Hugging Face
10model = CharEncoder.from_pretrained(REPO)
11tokenizer = CharTokenizer.from_pretrained(REPO)
12
13# Load precomputed embeddings + name lookup
14bin_path = hf_hub_download(REPO, "embeddings.bin")
15names_path = hf_hub_download(REPO, "embeddings_names.json")
16
17with open(bin_path, "rb") as f:
18 n, d = struct.unpack("<II", f.read(8))
19 embeddings = np.frombuffer(f.read(), dtype=np.float32).reshape(n, d)
20
21with open(names_path, encoding="utf-8") as f:
22 display_to_index = json.load(f)
23
24# Build alias -> (official_name, index) lookup
25idx_to_name = {}
26alias_to_officials = {}
27for disp, idx in display_to_index.items():
28 parts = [p.strip() for p in disp.split(":") if p.strip()]
29 if len(parts) == 2:
30 alias, official = parts
31 alias_to_officials.setdefault(alias.lower(), []).append(official)
32 idx = int(idx)
33 if idx not in idx_to_name and len(parts) == 2:
34 idx_to_name[idx] = parts[1]
35
36# Encode a query
37def search(query, top_k=5):
38 enc = tokenizer.encode_one(query)
39 with torch.no_grad():
40 q = model(enc["input_ids"].unsqueeze(0), enc["attention_mask"].unsqueeze(0))
41 q = q.squeeze(0).numpy()
42 q = q / np.linalg.norm(q)
43 sims = embeddings @ q # cosine similarity (embeddings are already normalized)
44 order = np.argsort(sims)[::-1]
45 seen = set()
46 results = []
47 for i in order:
48 name = idx_to_name.get(i)
49 if name and name not in seen:
50 seen.add(name)
51 results.append((name, float(sims[i])))
52 if len(results) >= top_k:
53 break
54 return results
55
56# Try it
57for q in ["choachukang", "cck", "block 209", "blk 209", "bedok"]:
58 print(f"\n{q}:")
59 for name, score in search(q):
60 print(f" {name:<30} {score:.4f}")
Trained on Singapore bus stops from
LTA DataMall
and MRT/LRT station data. The training CSV is generated by applying
deterministic augmentation rules (prefixes, joined words, initialisms,
abbreviation expansion/reduction, vowel-drop, and keyboard-adjacent typos) to
~5.6k official names, producing ~260k positive pairs.
Data sourced from the Land Transport Authority (LTA) of Singapore.