パスポートのヘボン式でYOKOOが崩れる — 長音はかな単位で判定

パスポートの氏名は、原則として外務省のヘボン式ローマ字で書く。規則は短く、「ん」は B・M・P の前で M、「っ」は子音を重ねる(CH の前だけ T)、長音の O や U は書かない、の 3 つでほとんど済む。ところが 3 つ目の「長音を書かない」を、ローマ字に変換したあとの文字列置換で実装すると、規則どおりの例を一部取りこぼす。

この記事では、ハシトシステムのローマ字変換ツール の実装を都道府県の旅券窓口が示す例 30 件に当てて、どこで外れるかを確かめる。そのうえで、長音をかな単位で判定する実装を Node.js で書き、同じ例で検算する。

長音の規則は「末尾のお」と「長音でないう」に例外がある

外務省のヘボン式綴方表そのものはこの記事の作成時に取得できなかったため、各都道府県の旅券窓口が同じ規則を説明しているページを一次資料として使う。

福岡県の案内 は、長音の O・U・H を原則として書かないとしたうえで、末尾が「おお」でヨミが「お」の場合は OO と書くという例外を示している。例は「よこお」が YOKOO、「せのお」が SENOO である。

神奈川県の案内 は、同じことを位置で整理している。

規則 例
末尾の長音の「お」は O と書く みそのお MISONOO、たかとお TAKATOO
末尾以外の長音の「お」は書かない おおの ONO、おおおか OOKA、こおり KORI
長音の「う」は末尾でも書かない みそのう MISONO、さとう SATO、ゆうか YUKA
長音ではない読み まつうら MATSUURA、こうちわ KOUCHIWA

「OH」で長音を表す書き方(OHNO など)は、福岡県の案内では非ヘボン式の扱いになるとされている。

ツールの実装: ローマ字にしてから ou・oo・uu を縮める

ローマ字変換ツールの convert は、かなを 1 文字ずつ(拗音は 2 文字で)ローマ字に置き換え、最後に長音をまとめて縮めている。

1
2
3
4
if(!keepLong){
// パスポート用ヘボン式の原則: 長音は表記しない(ou/oo/uu を短く)
out=out.replace(/ou/g,'o').replace(/oo/g,'o').replace(/uu/g,'u');
}

この方式は、さとう・ようこ・とおる・しょうじのような大半の例を正しく扱える。ただし、置換の時点ではすでにかなの区切りも位置も失われているので、次の 2 種類を区別できない。

  • 末尾の「お」: yokoo の oo も、ono(おおの)の元になった oono の oo と同じように 1 文字に縮められる。
  • 長音ではない o+u / u+u: まつうら は matsuura、こうちわ は kouchiwa になった時点で、長音の uu・ou と見分けがつかない。

旅券窓口の例 30 件に当てる

ツールの HTML から convert をそのまま取り出し、福岡県と神奈川県の例 30 件と突き合わせた(Node.js 24.11.1)。外れたのは次の 6 件だった。

1
2
3
4
5
6
7
NG よこお   YOKO     want YOKOO
NG せのお SENO want SENOO
NG みそのお MISONO want MISONOO
NG たかとお TAKATO want TAKATOO
NG まつうら MATSURA want MATSUURA
NG こうちわ KOCHIWA want KOUCHIWA
NG 6 / 30

撥音(なんば NAMBA、ほんま HOMMA、さんぺい SAMPEI)と促音(はっとり HATTORI、はっちょう HATCHO、えっちゅう ETCHU、ほっち HOTCHI)は 11 件すべて合っている。外れたのは長音の 6 件だけで、前半 4 件は「末尾のお」、後半 2 件は「長音ではない読み」にあたる。

なお、おおおか(大岡)は oooka に対して /oo/g が先頭の 2 文字だけを縮めるので、結果は OOKA で、たまたま正しい。

かな単位で判定する

前半 4 件は、かなの段階で判定すれば直せる。規則をそのままコードにすると次のようになる。

  • 「う」の直前の拍が お段か う段なら、その「う」は書かない(末尾でも同じ)。
  • 「お」の直前の拍が お段なら、その「お」は書かない。ただし語の末尾なら書く。
  • 長音として吸収した拍の次の拍は、吸収しない(おおおか を O + OKA と読ませるため)。

後半 2 件は、かなだけでは原理的に決まらない。「まつうら」の「つう」と「すうじ」の「すう」は同じ並びで、違いは読みの切れ目(まつ + うら)にしかない。そこで、入力側で切れ目を | で示せるようにし、切れ目の前後では長音として吸収しないことにする。

実際に試す

Node.js 24.11.1(Windows 11)で動作を確かめた。依存パッケージは要らない。次の内容を hepburn.mjs として保存する。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
// hepburn.mjs : パスポート用ヘボン式(長音の判定をかな単位で行う版)
const MONO = {
あ:'a', い:'i', う:'u', え:'e', お:'o', か:'ka', き:'ki', く:'ku', け:'ke', こ:'ko',
さ:'sa', し:'shi', す:'su', せ:'se', そ:'so', た:'ta', ち:'chi', つ:'tsu', て:'te', と:'to',
な:'na', に:'ni', ぬ:'nu', ね:'ne', の:'no', は:'ha', ひ:'hi', ふ:'fu', へ:'he', ほ:'ho',
ま:'ma', み:'mi', む:'mu', め:'me', も:'mo', や:'ya', ゆ:'yu', よ:'yo',
ら:'ra', り:'ri', る:'ru', れ:'re', ろ:'ro', わ:'wa', を:'o',
が:'ga', ぎ:'gi', ぐ:'gu', げ:'ge', ご:'go', ざ:'za', じ:'ji', ず:'zu', ぜ:'ze', ぞ:'zo',
だ:'da', ぢ:'ji', づ:'zu', で:'de', ど:'do', ば:'ba', び:'bi', ぶ:'bu', べ:'be', ぼ:'bo',
ぱ:'pa', ぴ:'pi', ぷ:'pu', ぺ:'pe', ぽ:'po',
};
const YOON = { ゃ:'a', ゅ:'u', ょ:'o' };

// かなを「拍」の配列にする。'|' は読みの切れ目(まつ|うら など)、空白は姓と名の区切り
function toMorae(kana) {
const s = kana.replace(/[ァ-ヶ]/g, c => String.fromCharCode(c.charCodeAt(0) - 0x60));
const out = [];
for (let i = 0; i < s.length; i++) {
const c = s[i], n = s[i + 1];
if (c === '|' || c === ' ' || c === ' ') { out.push({ sep: c === '|' ? '' : ' ' }); continue; }
if (c === 'っ' || c === 'ん') { out.push({ special: c }); continue; }
if (!MONO[c]) throw new Error('unsupported kana: ' + c);
if (YOON[n] && MONO[c].endsWith('i') && MONO[c].length > 1) {
const base = MONO[c].slice(0, -1); // ki -> k, shi -> sh, chi -> ch, ji -> j
const head = /^(sh|ch|j)$/.test(base) ? base : base + 'y';
out.push({ r: head + YOON[n], kana: c + n }); i++; continue;
}
out.push({ r: MONO[c], kana: c });
}
return out;
}

export function hepburn(kana) {
const m = toMorae(kana);
let res = '';
let prevVowel = ''; // 直前に書き出した拍の母音
let absorbed = false; // 直前の拍が長音として吸収されたか
for (let i = 0; i < m.length; i++) {
const x = m[i];
if (x.sep !== undefined) { res += x.sep; prevVowel = ''; absorbed = false; continue; }
const next = m[i + 1];
const atEnd = !next || next.sep !== undefined;
if (x.special === 'ん') {
const h = next && next.r ? next.r[0] : '';
res += 'bmp'.includes(h) && h ? 'm' : 'n'; prevVowel = ''; absorbed = false; continue;
}
if (x.special === 'っ') {
const r = next && next.r ? next.r : '';
res += r.startsWith('ch') ? 't' : r[0]; continue;
}
// 長音: お段・う段の後の「う」は末尾でも書かない。お段の後の「お」は末尾だけ書く
const isLongU = x.kana === 'う' && (prevVowel === 'o' || prevVowel === 'u');
const isLongO = x.kana === 'お' && prevVowel === 'o' && !atEnd;
if (!absorbed && (isLongU || isLongO)) { absorbed = true; continue; }
res += x.r; prevVowel = x.r.slice(-1); absorbed = false;
}
return res.toUpperCase();
}

// 外務省ヘボン式の例として各都道府県旅券窓口が示しているもの
const cases = [
['おおの', 'ONO'], ['かとう', 'KATO'], ['しょうじ', 'SHOJI'], ['ひゅうが', 'HYUGA'], ['とおる', 'TORU'],
['よこお', 'YOKOO'], ['せのお', 'SENOO'], ['みそのお', 'MISONOO'], ['たかとお', 'TAKATOO'],
['おおおか', 'OOKA'], ['こおり', 'KORI'], ['みそのう', 'MISONO'],
['なんば', 'NAMBA'], ['ほんま', 'HOMMA'], ['さんぺい', 'SAMPEI'], ['しんや', 'SHINYA'],
['はっとり', 'HATTORI'], ['はっちょう', 'HATCHO'], ['えっちゅう', 'ETCHU'], ['うりう', 'URIU'],
['まつ|うら', 'MATSUURA'], ['こ|うちわ', 'KOUCHIWA'], ['さいとう しんぺい', 'SAITO SHIMPEI'],
];
let ng = 0;
for (const [k, want] of cases) {
const got = hepburn(k);
if (got !== want) ng++;
console.log(`${got === want ? 'OK' : 'NG'} ${k} ${got} (want ${want})`);
}
console.log(`NG ${ng} / ${cases.length}`);
// 切れ目を入れないと、かなだけでは長音と区別できない
console.log('まつうら ->', hepburn('まつうら'), ' こうちわ ->', hepburn('こうちわ'));
1
node hepburn.mjs

出力(後半は省略):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
OK  おおの  ONO  (want ONO)
OK かとう KATO (want KATO)
...
OK よこお YOKOO (want YOKOO)
OK せのお SENOO (want SENOO)
OK みそのお MISONOO (want MISONOO)
OK たかとお TAKATOO (want TAKATOO)
OK おおおか OOKA (want OOKA)
...
OK まつ|うら MATSUURA (want MATSUURA)
OK こ|うちわ KOUCHIWA (want KOUCHIWA)
OK さいとう しんぺい SAITO SHIMPEI (want SAITO SHIMPEI)
NG 0 / 23
まつうら -> MATSURA こうちわ -> KOCHIWA

| を入れた 2 件は MATSUURA・KOUCHIWA になり、入れない場合は文字列置換版と同じく MATSURA・KOCHIWA になる。かなだけを入力にする限り、この 2 件はどの実装でも決められない。

実装するときの注意

  • 長音の判定はローマ字にする前に行う。ローマ字列の oo は「お段の拍 + お」なのか「お + お」なのか、語の末尾なのかを失っている。
  • 読みの切れ目を入力で受け取れるようにする。まつうら・こうちわのように、長音か別の拍かは読み(漢字の区切り)で決まる。切れ目を受け取れないなら、少なくとも「長音を省かない」出力も並べて、利用者が選べるようにする。
  • 最終判断は旅券窓口の一覧表で行う。神奈川県の案内は、示した例以外の表記を希望する場合は非ヘボン式になるとしている。変換結果は下書きとして使い、申請前に窓口の一覧で確かめる。

まとめ

  • パスポート用ヘボン式の長音は「書かない」が原則だが、末尾の「お」は O を書き(YOKOO)、まつうら・こうちわのように長音ではない読みは残す。
  • ローマ字列の ou・oo・uu を置換する実装は、旅券窓口の例 30 件中 6 件を外した。うち 4 件はかな単位で判定すれば直る。
  • 残る 2 件は読みの切れ目がないと決まらないので、入力に切れ目を持たせる。

ツール: ローマ字変換(ハシトシステム)。ヘボン式と訓令式を切り替えられ、「長音をそのまま伸ばす」にチェックを入れると長音を省かない出力も確かめられる。数字の表記変換は「漢数字変換をBigIntで書く」で扱った。


パスポートのヘボン式でYOKOOが崩れる — 長音はかな単位で判定
https://blog.hashito.biz/2026/10/09/passport-hepburn-long-vowel-trailing-o-matsuura-kana-boundary/
著者
hashito
作成日
2026年10月9日
著作権