DEV Community

Cover image for Solving the "Blind Continuation" Typing Problem: Building a Zero-Latency Vector Search for the Edge
Kaloyan Karaivanov
Kaloyan Karaivanov

Posted on • Originally published at github.com

Solving the "Blind Continuation" Typing Problem: Building a Zero-Latency Vector Search for the Edge

1. The Hook: The UI/UX Nightmare of Strict Dropdowns

Every front-end developer has faced this UI/UX dilemma: You have an input field where the user must select an item from a predefined list. It could be a list of countries for a shipping address, a specific product model for a customer support ticket, or a medical condition from a registry.

Predictive text is disabled. Free-form text is invalid. The user has to find the exact match.

Image 1: The UI/UX Frustration (The Hook)

Here is the most common scenario: The user knows exactly what they are looking for (e.g., “Switzerland”). They type fast. Their finger slips on the second character, typing “Swti…”. Instantly, the dropdown goes blank. “No results found.” The user is forced to stop, look at the screen, hit backspace multiple times, and re-type. This friction disrupts the flow and drastically degrades the user experience.

We needed a search engine that forgives human nature. Thus, the journey to build a better algorithm began.

2. The Analytical Breakdown: Why Existing Algorithms Fail at Typeahead

When tasked with building a typo-tolerant search for a predefined list, developers usually reach for standard tools. Let’s analyze why they fall short in a real-time, keystroke-by-keystroke scenario:

  • Strict Prefix Matching (Tries / Prefix Trees): They are blazingly fast but utterly unforgiving. The moment a user types Swt instead of Swi, the search space collapses to zero.
  • Levenshtein Distance: The gold standard for spellchecking. However, calculating the edit distance for thousands of items on every single keystroke is computationally heavy ($O(N \cdot M)$) and can easily block the main UI thread. Furthermore, it suffers from severe “length penalty” issues when comparing incomplete typed prefixes against long target words.
  • Server-Side Engines (Algolia / Typesense / Elasticsearch): These are incredibly powerful but introduce network latency. In a mobile or web app, requiring a network round-trip just to suggest “Switzerland” feels sluggish.

Image 2: Server Latency vs. Edge Computing (Architecture

We needed an Edge Computing solution — something that lives entirely in the device’s memory, updates the UI instantly, and operates with O(1) time complexity.

3. The “Blind Continuation” Phenomenon

The fundamental flaw in traditional algorithms is that they treat typeahead as a sequence of whole words, rather than a fluid human action. We identified a behavioral phenomenon we call “Blind Continuation.”

When users make a typo early in a word, they rarely stop immediately. They instinctively continue typing the rest of the word correctly (e.g., Swtizerland). We needed an algorithm capable of recognizing that the momentum of the sequence is correct, even if the prefix is fractured.

4. The Mathematical Solution: Vectorizing the Typeahead

To solve this, we built typeahead-kmp—an open-source library for Kotlin Multiplatform. Instead of comparing strings dynamically on every keystroke, we mathematically model human typing errors using L2-Normalized Sparse Vector Spaces.
Here is how the algorithm works under the hood during the initialization phase:

  • P0 Anchor: The very first letter is almost never mistyped. We anchor it with massive mathematical weight.
  • Fuzzy Prefixes: To handle transpositions (e.g., Swt instead of Swi), we anchor the first letter and alphabetically sort the rest of the current prefix. Thus, the typed input and the target generate the exact same spatial feature.
  • Typoglycemia Gestalt: If the user types a word with the exact same length, first letter, and last letter as a target in the database, the engine temporarily switches to “Spellchecker Mode,” applying a massive multiplier for this perfect structural match.
  • Skip-Grams & Floating N-Grams: Overlapping character sequences bridge the gap over missed or extra letters, heavily rewarding the “momentum” of correct typing.

Image 3: The Vector Space Math (Under the Hood)<br>

Because all items are pre-vectorized and L2-normalized (the length of every vector is exactly 1.0), the find() operation is reduced to an incredibly simple Cosine Similarity (Dot Product) calculation. It is just multiplication and addition, operating in O(1) time per feature.

5. Real-World Simulations: The Algorithm in Action

Let’s look at how our vector engine dynamically reacts to keystrokes in three real-world scenarios, based on our production test logs:

Image 4: The Fuzzy Prefix Logic (Real-world Simulation)<br>

Scenario 1: Transposition (Swapped Letters)

The user is searching for Switzerland, but accidentally swaps the second and third letters: Swtizerland.

=== Typing: 'Swt' with typing error of 'Swtizerland' ===
1. Sweden - Score: 0.13587454592998566
2. Switzerland - Score: 0.096374973600844775 <-- Holds on via Fuzzy Prefix!
3. Serbia - Score: 0.03017046311660885
...

=== Typing: 'Swtizer' with typing error of 'Swtizerland' ===
1. Switzerland - Score: 0.31485594284874635 <-- Rockets to #1 via structural momentum!
2. Sweden - Score: 0.05020885653648152
3. Syria - Score: 0.02109225755026867
Enter fullscreen mode Exit fullscreen mode

Analysis: Despite the strict prefix breaking at the third letter, the algorithm finds an intersection via Fuzzy Prefixes. When the user blindly continues typing (izer), the overlapping Skip-Grams massively intersect with the original word, propelling it to the top.

Typing: 'Swtizer' with typing error of 'Swtizerland'

Scenario 2: Deletion (Missed Letter)

The user is searching for Liechtenstein, but misses the first ‘e’: Lichtenstein.

=== Typing: 'Lic' with typing error of 'Lichtenstein' ===
1. Libya - Score: 0.16997965972507267
2. Liberia - Score: 0.11166355040347513
3. Liechtenstein - Score: 0.09315516087569103
...

=== Typing: 'Lichtens' with typing error of 'Lichtenstein' ===
1. Liechtenstein - Score: 0.14427532224705913 <-- Takes the absolute lead!
2. Libya - Score: 0.0527502899922212
3. Liberia - Score: 0.0346528794967077
Enter fullscreen mode Exit fullscreen mode

Analysis: Here, the N-Grams work their magic. Even though a letter is missing, the remaining characters form an unbreakable structural spine. The Cosine Similarity forces the longer, structurally accurate word to overtake shorter words like Libya.

Typing: 'Lichtens' with typing error of 'Lichtenstein'

Scenario 3: Insertion (Extra Letter / Double Consonant)

The user is searching for Philippines, but mistakenly doubles the ‘l’: Phillipines.

=== Typing: 'Phil' with typing error of 'Phillipines' ===
1. Philippines - Score: 0.23192004246067098 <-- Takes a commanding early lead!
2. Peru - Score: 0.04363297592453538
...

=== Typing: 'Phillip' with typing error of 'Phillipines' ===
1. Philippines - Score: 0.20281260731949163 <-- Maintains #1 despite the extra 'l'
2. Peru - Score: 0.027270059033965625
3. Chile - Score: 0.021972209426843504
Enter fullscreen mode Exit fullscreen mode

Analysis: The algorithm gracefully steps over the inserted keystroke. Skip-grams inherently ignore the accidental extra letter, ensuring the user is never penalized for common phonetic or double-consonant mistakes.

Typing: 'Phillip' with typing error of 'Phillipines'

6. Memory and Performance Mastery

Building an in-memory vector database on a mobile device requires extreme memory efficiency. We designed the architecture to protect the device’s RAM and CPU:

  • Lock-Free Concurrent Priority Queue: Instead of sorting massive arrays on every keystroke, we maintain a bounded priority queue (e.g., Top 10) using StateFlow and atomic Compare-And-Swap (CAS) operations. This eliminates thread-blocking and instantly discards low-scoring results with zero-allocation.
  • Stream-Based Serialization (Lazy Sequences): Vectorizing thousands of items on startup can be CPU-intensive. Our library allows exporting the computed sparse vectors via lazy Kotlin Sequence flows. You can save the state to a local file and import it instantly on the next app launch, completely bypassing the heavy math and preventing Out-Of-Memory (OOM) errors.

7. Implementation: Open Source for KMP

We have open-sourced this exact engine for the Kotlin Multiplatform ecosystem. It works natively across Android, iOS, Desktop, and Web (WASM/JS).

import io.github.karloti.typeahead.TypeaheadSearchEngine

// 1. Initialize the engine
val searchEngine = TypeaheadSearchEngine<Country>(textSelector = { it.name })

// 2. Load items asynchronously (distributes vectorization across CPU cores)
searchEngine.addAll(countriesList)

// 3. Search instantly
val results = searchEngine.find("Swtizerland", maxResults = 5)
Enter fullscreen mode Exit fullscreen mode

Conclusion

Search is not just about comparing strings; it is about understanding human intent and the physics of typing. By moving away from traditional string-distance algorithms and embracing L2-normalized vector spaces on the edge, we can build user interfaces that feel magical, instant, and completely forgiving.

Check out the repository, star it, and try it in your next KMP project:

🔗 github.com/karloti/typeahead-kmp

Top comments (0)