◉GEMINI LABJP
●ARGON — gemini-4-argon is generally available as of 10/13, aimed at complex coding, agents, math and science●DEEP RES — The old Deep Research agent shut down on 10/23. If calls fail, move to deep-research-preview-04-2026●NANO — gemini-nano-banana-2.1 is GA (10/06). gemini-3.1-flash-image is deprecated with no shutdown date yet●MCP — One write-up cut an 85,000-token tool result to 310 tokens by wrapping it in an ADK FunctionTool●FLASH — gemini-3.7-flash and 3.5-flash now route automatically to newer Flash models (10/08)●NEW — Stop Re-Reading Every Crash Report: Gre…●ARGON — gemini-4-argon is generally available as of 10/13, aimed at complex coding, agents, math and science●DEEP RES — The old Deep Research agent shut down on 10/23. If calls fail, move to deep-research-preview-04-2026●NANO — gemini-nano-banana-2.1 is GA (10/06). gemini-3.1-flash-image is deprecated with no shutdown date yet●MCP — One write-up cut an 85,000-token tool result to 310 tokens by wrapping it in an ADK FunctionTool●FLASH — gemini-3.7-flash and 3.5-flash now route automatically to newer Flash models (10/08)●NEW — Stop Re-Reading Every Crash Report: Gre…
Articles/Dev Tools
⟐ Dev Tools/2026-09-10Intermediate

Matching name variants in a print catalog: normalize first, then ask Gemini for candidates

When the same artist appears under three spellings, asking Gemini whether two names are the same person quietly merges different generations. Here is the two-layer alternative — deterministic normalization first, model-generated candidates second — with working code and a cost estimate.

Gemini API251Structured Output11Data CleanupIndie Development16Ukiyo-e

✦ Premium Article

I was reading through the catalog one evening, before a batch of ukiyo-e wallpapers went out, when I noticed the same artist sitting in three different rows. "歌川広重", "安藤広重", "Utagawa Hiroshige". None of them is wrong. To my script, they were three different people.

I scan old woodblock prints and repair the creases and stains by hand, one sheet at a time. That part of the work has not changed tools. The metadata I attach afterwards — artist name, title, series — is typed by a person, so it drifts.

My first attempt was to have Gemini absorb that drift. It took me a while to understand why that was the wrong shape.

What happened the day I asked "are these the same person?"

The first function I wrote handed two names to the model and asked whether they referred to the same artist. Most of the time the answer was right. The problem lived in the handful of cases where it was not.

Hiroshige and the pupil who later took the name as the second Hiroshige came back as one person. The answer arrived as a flat "yes, the same artist," so in a spreadsheet it looked exactly like every correct answer. I shipped a few prints with the wrong attribution before I caught it.

The model supplies material; I make the call. Getting to that line helped, but stopping the yes/no question was not enough on its own. The real waste was the number of calls. I was asking a language model to re-derive the same spelling drift, row after row.

Layer one: normalization belongs in code

Most of the drift is not a question of meaning. Full-width and half-width characters, old and new kanji forms, Hepburn long vowels, parenthetical notes — all of it is mechanical. There is no reason to spend inference on it.

# normalize.py — collapse everything a rule can collapse, before any API call
import re
import unicodedata
 
# old/variant kanji forms -> modern forms (I add entries as the catalog surfaces them)
KYUJI = str.maketrans({
    "廣": "広", "國": "国", "齋": "斎", "澤": "沢",
    "溪": "渓", "藝": "芸", "驛": "駅", "豐": "豊",
})
 
NUM_FIX = [("拾", "十"), ("卅", "三十")]
 
# parentheses, trailing role markers, circled numerals
DECOR = re.compile(r"[((\[][^))\]]*[))\]]|画$|筆$|[①-⑳]")
 
 
def normalize(name: str) -> str:
    s = unicodedata.normalize("NFKC", name)      # unify full-width and half-width forms
    s = s.translate(KYUJI)
    for a, b in NUM_FIX:
        s = s.replace(a, b)
    s = DECOR.sub("", s)
    s = re.sub(r"[\s・,.'’\-]", "", s)            # drop separators entirely
    return s.lower()
 
 
def romaji_key(name: str) -> str:
    """Collapse long-vowel spellings: Hiroshige / Hirōshige / Hiroshige."""
    s = unicodedata.normalize("NFKD", name)
    s = "".join(c for c in s if not unicodedata.combining(c))  # ō -> o
    s = re.sub(r"(ou|oo|uu)", lambda m: m.group(0)[0], s.lower())
    return re.sub(r"[^a-z]", "", s)
 
 
if __name__ == "__main__":
    for a, b in [("歌川廣重", "歌川広重"), ("Utagawa Hirōshige", "Utagawa Hiroshige")]:
        key = normalize if re.search(r"[ぁ-んァ-ヶ一-龥]", a) else romaji_key
        print(a, b, key(a) == key(b))
        # expected output:
        # 歌川廣重 歌川広重 True
        # Utagawa Hirōshige Utagawa Hiroshige True

In my catalog, this function alone dropped most of the variants onto a single key. What survived were pairs like "歌川広重" and "安藤広重", which no amount of character folding will ever connect. That is where the model earns its place.

✦

Thank you for reading this far.

Continue Reading

What follows includes implementation code, benchmarks, and practical content we hope you'll find useful. This site runs without ads — server and development costs are supported entirely by members like you. If it's been helpful, we'd be truly grateful for your support.

WHAT YOU'LL LEARN
✦You will be able to draw the line yourself between what normalization should handle and what the model should handle, before a name-matching job breaks in a way nobody notices
✦You will have a concrete way to stabilize output using response_schema and system_instruction, instead of relying on a temperature setting that is no longer honored
✦You will know how to run a thousand-row reconciliation for around one dollar, mostly by cutting the number of calls you make at all
Secure payment via Stripe · Cancel anytime
✦

Unlock This Article

Get full access to the rest of this article. Buy once, read anytime. This site is ad-free — your support goes directly toward keeping it running.

or
Unlock all articles with Membership →
Share

Thank You for Reading

Gemini Lab is ad-free, supported entirely by members like you. We publish practical guides daily with implementation code, benchmarks, and production-ready patterns. If you've found it useful, we'd love to have you on board.

  • ✦Copy-paste ready implementation code
  • ✦New advanced guides published daily
  • ✦$5/mo or $10 for lifetime access
View Membership →

Related Articles

◈ API / SDK2026-06-12
Building an App Store Rejection Workflow with the Gemini API — From Structured Notices to Resolution Center Replies
How I use the Gemini API to parse App Store rejection notices into structured JSON, cross-check guidelines, draft Resolution Center replies, and run pre-submission checks as an indie developer.
⟐ Dev Tools2026-09-06
Icon-Only Buttons Can Stay Silent for Screen Readers, Even When Every Translation Is in Place
A fully translated app can still read as silence. Here is how I collect icon elements and labels from iOS and Android, settle everything mechanical locally, and send only the wording to Gemini — with the actual output from each stage.
⟐ Dev Tools2026-09-02
An agreement-rate gate approves the model swap, then your category shares move
Swapping the model behind an image classification batch can pass a golden-set agreement gate while category shares quietly shift. I measured the sample sizes each check really needs and rebuilt the gate as a paired comparison.