xref: /webtrees/app/Soundex.php (revision 9797fe2e6d6eb3d34559dcf546f47c58bc50777b)
1<?php
2/**
3 * webtrees: online genealogy
4 * Copyright (C) 2016 webtrees development team
5 * This program is free software: you can redistribute it and/or modify
6 * it under the terms of the GNU General Public License as published by
7 * the Free Software Foundation, either version 3 of the License, or
8 * (at your option) any later version.
9 * This program is distributed in the hope that it will be useful,
10 * but WITHOUT ANY WARRANTY; without even the implied warranty of
11 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 * GNU General Public License for more details.
13 * You should have received a copy of the GNU General Public License
14 * along with this program. If not, see <http://www.gnu.org/licenses/>.
15 */
16namespace Fisharebest\Webtrees;
17
18/**
19 * Phonetic matching of strings.
20 */
21class Soundex {
22	/**
23	 * Which algorithms are supported.
24	 *
25	 * @return string[]
26	 */
27	public static function getAlgorithms() {
28		return array(
29			'std' => /* I18N: http://en.wikipedia.org/wiki/Soundex */ I18N::translate('Russell'),
30			'dm'  => /* I18N: http://en.wikipedia.org/wiki/Daitch–Mokotoff_Soundex */ I18N::translate('Daitch-Mokotoff'),
31		);
32	}
33
34	/**
35	 * Is there a match between two soundex codes?
36	 *
37	 * @param string $soundex1
38	 * @param string $soundex2
39	 *
40	 * @return bool
41	 */
42	public static function compare($soundex1, $soundex2) {
43		if ($soundex1 && $soundex2) {
44			foreach (explode(':', $soundex1) as $code) {
45				if (strpos($soundex2, $code) !== false) {
46					return true;
47				}
48			}
49		}
50
51		return false;
52	}
53
54	/**
55	 * Generate Russell soundex codes for a given text.
56	 *
57	 * @param $text
58	 *
59	 * @return null|string
60	 */
61	public static function russell($text) {
62		$words         = preg_split('/\s/', $text, -1, PREG_SPLIT_NO_EMPTY);
63		$soundex_array = array();
64		foreach ($words as $word) {
65			$soundex = soundex($word);
66			// Only return codes from recognisable sounds
67			if ($soundex !== '0000') {
68				$soundex_array[] = $soundex;
69			}
70		}
71		// Combine words, e.g. “New York” as “Newyork”
72		if (count($words) > 1) {
73			$soundex_array[] = soundex(strtr($text, ' ', ''));
74		}
75		// A varchar(255) column can only hold 51 4-character codes (plus 50 delimiters)
76		$soundex_array = array_slice(array_unique($soundex_array), 0, 51);
77
78		if ($soundex_array) {
79			return implode(':', $soundex_array);
80		} else {
81			return '';
82		}
83	}
84
85	/**
86	 * Generate Daitch–Mokotoff soundex codes for a given text.
87	 *
88	 * @param $text
89	 *
90	 * @return null|string
91	 */
92	public static function daitchMokotoff($text) {
93		$words         = preg_split('/\s/', $text, -1, PREG_SPLIT_NO_EMPTY);
94		$soundex_array = array();
95		foreach ($words as $word) {
96			$soundex_array = array_merge($soundex_array, self::daitchMokotoffWord($word));
97		}
98		// Combine words, e.g. “New York” as “Newyork”
99		if (count($words) > 1) {
100			$soundex_array = array_merge($soundex_array, self::daitchMokotoffWord(strtr($text, ' ', '')));
101		}
102		// A varchar(255) column can only hold 36 6-character codes (plus 35 delimiters)
103		$soundex_array = array_slice(array_unique($soundex_array), 0, 36);
104
105		if ($soundex_array) {
106			return implode(':', $soundex_array);
107		} else {
108			return '';
109		}
110	}
111
112	// Determine the Daitch–Mokotoff Soundex code for a word
113	// Original implementation by Gerry Kroll, and analysis by Meliza Amity
114
115	// Max. table key length (in ASCII bytes -- NOT in UTF-8 characters!)
116	const MAXCHAR = 7;
117
118	/**
119	 * Name transformation arrays.
120	 * Used to transform the Name string to simplify the "sounds like" table.
121	 * This is especially useful in Hebrew.
122	 *
123	 * Each array entry defines the "from" and "to" arguments of an preg($from, $to, $text)
124	 * function call to achieve the desired transformations.
125	 *
126	 * Note about the use of "\x01":
127	 * This code, which can’t legitimately occur in the kind of text we're dealing with,
128	 * is used as a place-holder so that conditional string replacements can be done.
129	 *
130	 * @var string[][]
131	 */
132	private static $transformNameTable = array(
133		// Force Yiddish ligatures to be treated as separate letters
134		array('װ', 'וו'),
135		array('ײ', 'יי'),
136		array('ױ', 'וי'),
137		array('בו', 'בע'),
138		array('פו', 'פע'),
139		array('ומ', 'עמ'),
140		array('ום', 'עם'),
141		array('ונ', 'ענ'),
142		array('ון', 'ען'),
143		array('וו', 'ב'),
144		array("\x01", ''),
145		array('ייה$', "\x01ה"),
146		array('ייע$', "\x01ע"),
147		array('יי', 'ע'),
148		array("\x01", 'יי'),
149	);
150
151	/**
152	 * The DM sound coding table is organized this way:
153	 * key: a variable-length string that corresponds to the UTF-8 character sequence
154	 * represented by the table entry. Currently, that string can be up to 7
155	 * bytes long. This maximum length is defined by the value of global variable
156	 * $maxchar.
157	 *
158	 * value: an array as follows:
159	 * [0]:  zero if not a vowel
160	 * [1]:  sound value when this string is at the beginning of the word
161	 * [2]:  sound value when this string is followed by a vowel
162	 * [3]:  sound value for other cases
163	 * [1],[2],[3] can be repeated several times to create branches in the code
164	 * an empty sound value means "ignore in this state"
165	 *
166	 * @var string[][]
167	 */
168	private static $dmsounds = array(
169		'A'       => array('1', '0', '', ''),
170		'À'       => array('1', '0', '', ''),
171		'Á'       => array('1', '0', '', ''),
172		'Â'       => array('1', '0', '', ''),
173		'Ã'       => array('1', '0', '', ''),
174		'Ä'       => array('1', '0', '1', '', '0', '', ''),
175		'Å'       => array('1', '0', '', ''),
176		'Ă'       => array('1', '0', '', ''),
177		'Ą'       => array('1', '', '', '', '', '', '6'),
178		'Ạ'       => array('1', '0', '', ''),
179		'Ả'       => array('1', '0', '', ''),
180		'Ấ'       => array('1', '0', '', ''),
181		'Ầ'       => array('1', '0', '', ''),
182		'Ẩ'       => array('1', '0', '', ''),
183		'Ẫ'       => array('1', '0', '', ''),
184		'Ậ'       => array('1', '0', '', ''),
185		'Ắ'       => array('1', '0', '', ''),
186		'Ằ'       => array('1', '0', '', ''),
187		'Ẳ'       => array('1', '0', '', ''),
188		'Ẵ'       => array('1', '0', '', ''),
189		'Ặ'       => array('1', '0', '', ''),
190		'AE'      => array('1', '0', '1', ''),
191		'Æ'       => array('1', '0', '1', ''),
192		'AI'      => array('1', '0', '1', ''),
193		'AJ'      => array('1', '0', '1', ''),
194		'AU'      => array('1', '0', '7', ''),
195		'AV'      => array('1', '0', '7', '', '7', '7', '7'),
196		'ÄU'      => array('1', '0', '1', ''),
197		'AY'      => array('1', '0', '1', ''),
198		'B'       => array('0', '7', '7', '7'),
199		'C'       => array('0', '5', '5', '5', '34', '4', '4'),
200		'Ć'       => array('0', '4', '4', '4'),
201		'Č'       => array('0', '4', '4', '4'),
202		'Ç'       => array('0', '4', '4', '4'),
203		'CH'      => array('0', '5', '5', '5', '34', '4', '4'),
204		'CHS'     => array('0', '5', '54', '54'),
205		'CK'      => array('0', '5', '5', '5', '45', '45', '45'),
206		'CCS'     => array('0', '4', '4', '4'),
207		'CS'      => array('0', '4', '4', '4'),
208		'CSZ'     => array('0', '4', '4', '4'),
209		'CZ'      => array('0', '4', '4', '4'),
210		'CZS'     => array('0', '4', '4', '4'),
211		'D'       => array('0', '3', '3', '3'),
212		'Ď'       => array('0', '3', '3', '3'),
213		'Đ'       => array('0', '3', '3', '3'),
214		'DRS'     => array('0', '4', '4', '4'),
215		'DRZ'     => array('0', '4', '4', '4'),
216		'DS'      => array('0', '4', '4', '4'),
217		'DSH'     => array('0', '4', '4', '4'),
218		'DSZ'     => array('0', '4', '4', '4'),
219		'DT'      => array('0', '3', '3', '3'),
220		'DDZ'     => array('0', '4', '4', '4'),
221		'DDZS'    => array('0', '4', '4', '4'),
222		'DZ'      => array('0', '4', '4', '4'),
223		'DŹ'      => array('0', '4', '4', '4'),
224		'DŻ'      => array('0', '4', '4', '4'),
225		'DZH'     => array('0', '4', '4', '4'),
226		'DZS'     => array('0', '4', '4', '4'),
227		'E'       => array('1', '0', '', ''),
228		'È'       => array('1', '0', '', ''),
229		'É'       => array('1', '0', '', ''),
230		'Ê'       => array('1', '0', '', ''),
231		'Ë'       => array('1', '0', '', ''),
232		'Ĕ'       => array('1', '0', '', ''),
233		'Ė'       => array('1', '0', '', ''),
234		'Ę'       => array('1', '', '', '6', '', '', ''),
235		'Ẹ'       => array('1', '0', '', ''),
236		'Ẻ'       => array('1', '0', '', ''),
237		'Ẽ'       => array('1', '0', '', ''),
238		'Ế'       => array('1', '0', '', ''),
239		'Ề'       => array('1', '0', '', ''),
240		'Ể'       => array('1', '0', '', ''),
241		'Ễ'       => array('1', '0', '', ''),
242		'Ệ'       => array('1', '0', '', ''),
243		'EAU'     => array('1', '0', '', ''),
244		'EI'      => array('1', '0', '1', ''),
245		'EJ'      => array('1', '0', '1', ''),
246		'EU'      => array('1', '1', '1', ''),
247		'EY'      => array('1', '0', '1', ''),
248		'F'       => array('0', '7', '7', '7'),
249		'FB'      => array('0', '7', '7', '7'),
250		'G'       => array('0', '5', '5', '5', '34', '4', '4'),
251		'Ğ'       => array('0', '', '', ''),
252		'GGY'     => array('0', '5', '5', '5'),
253		'GY'      => array('0', '5', '5', '5'),
254		'H'       => array('0', '5', '5', '', '5', '5', '5'),
255		'I'       => array('1', '0', '', ''),
256		'Ì'       => array('1', '0', '', ''),
257		'Í'       => array('1', '0', '', ''),
258		'Î'       => array('1', '0', '', ''),
259		'Ï'       => array('1', '0', '', ''),
260		'Ĩ'       => array('1', '0', '', ''),
261		'Į'       => array('1', '0', '', ''),
262		'İ'       => array('1', '0', '', ''),
263		'Ỉ'       => array('1', '0', '', ''),
264		'Ị'       => array('1', '0', '', ''),
265		'IA'      => array('1', '1', '', ''),
266		'IE'      => array('1', '1', '', ''),
267		'IO'      => array('1', '1', '', ''),
268		'IU'      => array('1', '1', '', ''),
269		'J'       => array('0', '1', '', '', '4', '4', '4', '5', '5', ''),
270		'K'       => array('0', '5', '5', '5'),
271		'KH'      => array('0', '5', '5', '5'),
272		'KS'      => array('0', '5', '54', '54'),
273		'L'       => array('0', '8', '8', '8'),
274		'Ľ'       => array('0', '8', '8', '8'),
275		'Ĺ'       => array('0', '8', '8', '8'),
276		'Ł'       => array('0', '7', '7', '7', '8', '8', '8'),
277		'LL'      => array('0', '8', '8', '8', '58', '8', '8', '1', '8', '8'),
278		'LLY'     => array('0', '8', '8', '8', '1', '8', '8'),
279		'LY'      => array('0', '8', '8', '8', '1', '8', '8'),
280		'M'       => array('0', '6', '6', '6'),
281		'MĔ'      => array('0', '66', '66', '66'),
282		'MN'      => array('0', '66', '66', '66'),
283		'N'       => array('0', '6', '6', '6'),
284		'Ń'       => array('0', '6', '6', '6'),
285		'Ň'       => array('0', '6', '6', '6'),
286		'Ñ'       => array('0', '6', '6', '6'),
287		'NM'      => array('0', '66', '66', '66'),
288		'O'       => array('1', '0', '', ''),
289		'Ò'       => array('1', '0', '', ''),
290		'Ó'       => array('1', '0', '', ''),
291		'Ô'       => array('1', '0', '', ''),
292		'Õ'       => array('1', '0', '', ''),
293		'Ö'       => array('1', '0', '', ''),
294		'Ø'       => array('1', '0', '', ''),
295		'Ő'       => array('1', '0', '', ''),
296		'Œ'       => array('1', '0', '', ''),
297		'Ơ'       => array('1', '0', '', ''),
298		'Ọ'       => array('1', '0', '', ''),
299		'Ỏ'       => array('1', '0', '', ''),
300		'Ố'       => array('1', '0', '', ''),
301		'Ồ'       => array('1', '0', '', ''),
302		'Ổ'       => array('1', '0', '', ''),
303		'Ỗ'       => array('1', '0', '', ''),
304		'Ộ'       => array('1', '0', '', ''),
305		'Ớ'       => array('1', '0', '', ''),
306		'Ờ'       => array('1', '0', '', ''),
307		'Ở'       => array('1', '0', '', ''),
308		'Ỡ'       => array('1', '0', '', ''),
309		'Ợ'       => array('1', '0', '', ''),
310		'OE'      => array('1', '0', '', ''),
311		'OI'      => array('1', '0', '1', ''),
312		'OJ'      => array('1', '0', '1', ''),
313		'OU'      => array('1', '0', '', ''),
314		'OY'      => array('1', '0', '1', ''),
315		'P'       => array('0', '7', '7', '7'),
316		'PF'      => array('0', '7', '7', '7'),
317		'PH'      => array('0', '7', '7', '7'),
318		'Q'       => array('0', '5', '5', '5'),
319		'R'       => array('0', '9', '9', '9'),
320		'Ř'       => array('0', '4', '4', '4'),
321		'RS'      => array('0', '4', '4', '4', '94', '94', '94'),
322		'RZ'      => array('0', '4', '4', '4', '94', '94', '94'),
323		'S'       => array('0', '4', '4', '4'),
324		'Ś'       => array('0', '4', '4', '4'),
325		'Š'       => array('0', '4', '4', '4'),
326		'Ş'       => array('0', '4', '4', '4'),
327		'SC'      => array('0', '2', '4', '4'),
328		'ŠČ'      => array('0', '2', '4', '4'),
329		'SCH'     => array('0', '4', '4', '4'),
330		'SCHD'    => array('0', '2', '43', '43'),
331		'SCHT'    => array('0', '2', '43', '43'),
332		'SCHTCH'  => array('0', '2', '4', '4'),
333		'SCHTSCH' => array('0', '2', '4', '4'),
334		'SCHTSH'  => array('0', '2', '4', '4'),
335		'SD'      => array('0', '2', '43', '43'),
336		'SH'      => array('0', '4', '4', '4'),
337		'SHCH'    => array('0', '2', '4', '4'),
338		'SHD'     => array('0', '2', '43', '43'),
339		'SHT'     => array('0', '2', '43', '43'),
340		'SHTCH'   => array('0', '2', '4', '4'),
341		'SHTSH'   => array('0', '2', '4', '4'),
342		'ß'       => array('0', '', '4', '4'),
343		'ST'      => array('0', '2', '43', '43'),
344		'STCH'    => array('0', '2', '4', '4'),
345		'STRS'    => array('0', '2', '4', '4'),
346		'STRZ'    => array('0', '2', '4', '4'),
347		'STSCH'   => array('0', '2', '4', '4'),
348		'STSH'    => array('0', '2', '4', '4'),
349		'SSZ'     => array('0', '4', '4', '4'),
350		'SZ'      => array('0', '4', '4', '4'),
351		'SZCS'    => array('0', '2', '4', '4'),
352		'SZCZ'    => array('0', '2', '4', '4'),
353		'SZD'     => array('0', '2', '43', '43'),
354		'SZT'     => array('0', '2', '43', '43'),
355		'T'       => array('0', '3', '3', '3'),
356		'Ť'       => array('0', '3', '3', '3'),
357		'Ţ'       => array('0', '3', '3', '3', '4', '4', '4'),
358		'TC'      => array('0', '4', '4', '4'),
359		'TCH'     => array('0', '4', '4', '4'),
360		'TH'      => array('0', '3', '3', '3'),
361		'TRS'     => array('0', '4', '4', '4'),
362		'TRZ'     => array('0', '4', '4', '4'),
363		'TS'      => array('0', '4', '4', '4'),
364		'TSCH'    => array('0', '4', '4', '4'),
365		'TSH'     => array('0', '4', '4', '4'),
366		'TSZ'     => array('0', '4', '4', '4'),
367		'TTCH'    => array('0', '4', '4', '4'),
368		'TTS'     => array('0', '4', '4', '4'),
369		'TTSCH'   => array('0', '4', '4', '4'),
370		'TTSZ'    => array('0', '4', '4', '4'),
371		'TTZ'     => array('0', '4', '4', '4'),
372		'TZ'      => array('0', '4', '4', '4'),
373		'TZS'     => array('0', '4', '4', '4'),
374		'U'       => array('1', '0', '', ''),
375		'Ù'       => array('1', '0', '', ''),
376		'Ú'       => array('1', '0', '', ''),
377		'Û'       => array('1', '0', '', ''),
378		'Ü'       => array('1', '0', '', ''),
379		'Ũ'       => array('1', '0', '', ''),
380		'Ū'       => array('1', '0', '', ''),
381		'Ů'       => array('1', '0', '', ''),
382		'Ű'       => array('1', '0', '', ''),
383		'Ų'       => array('1', '0', '', ''),
384		'Ư'       => array('1', '0', '', ''),
385		'Ụ'       => array('1', '0', '', ''),
386		'Ủ'       => array('1', '0', '', ''),
387		'Ứ'       => array('1', '0', '', ''),
388		'Ừ'       => array('1', '0', '', ''),
389		'Ử'       => array('1', '0', '', ''),
390		'Ữ'       => array('1', '0', '', ''),
391		'Ự'       => array('1', '0', '', ''),
392		'UE'      => array('1', '0', '', ''),
393		'UI'      => array('1', '0', '1', ''),
394		'UJ'      => array('1', '0', '1', ''),
395		'UY'      => array('1', '0', '1', ''),
396		'UW'      => array('1', '0', '1', '', '0', '7', '7'),
397		'V'       => array('0', '7', '7', '7'),
398		'W'       => array('0', '7', '7', '7'),
399		'X'       => array('0', '5', '54', '54'),
400		'Y'       => array('1', '1', '', ''),
401		'Ý'       => array('1', '1', '', ''),
402		'Ỳ'       => array('1', '1', '', ''),
403		'Ỵ'       => array('1', '1', '', ''),
404		'Ỷ'       => array('1', '1', '', ''),
405		'Ỹ'       => array('1', '1', '', ''),
406		'Z'       => array('0', '4', '4', '4'),
407		'Ź'       => array('0', '4', '4', '4'),
408		'Ż'       => array('0', '4', '4', '4'),
409		'Ž'       => array('0', '4', '4', '4'),
410		'ZD'      => array('0', '2', '43', '43'),
411		'ZDZ'     => array('0', '2', '4', '4'),
412		'ZDZH'    => array('0', '2', '4', '4'),
413		'ZH'      => array('0', '4', '4', '4'),
414		'ZHD'     => array('0', '2', '43', '43'),
415		'ZHDZH'   => array('0', '2', '4', '4'),
416		'ZS'      => array('0', '4', '4', '4'),
417		'ZSCH'    => array('0', '4', '4', '4'),
418		'ZSH'     => array('0', '4', '4', '4'),
419		'ZZS'     => array('0', '4', '4', '4'),
420		// Cyrillic alphabet
421		'А'   => array('1', '0', '', ''),
422		'Б'   => array('0', '7', '7', '7'),
423		'В'   => array('0', '7', '7', '7'),
424		'Г'   => array('0', '5', '5', '5'),
425		'Д'   => array('0', '3', '3', '3'),
426		'ДЗ'  => array('0', '4', '4', '4'),
427		'Е'   => array('1', '0', '', ''),
428		'Ё'   => array('1', '0', '', ''),
429		'Ж'   => array('0', '4', '4', '4'),
430		'З'   => array('0', '4', '4', '4'),
431		'И'   => array('1', '0', '', ''),
432		'Й'   => array('1', '1', '', '', '4', '4', '4'),
433		'К'   => array('0', '5', '5', '5'),
434		'Л'   => array('0', '8', '8', '8'),
435		'М'   => array('0', '6', '6', '6'),
436		'Н'   => array('0', '6', '6', '6'),
437		'О'   => array('1', '0', '', ''),
438		'П'   => array('0', '7', '7', '7'),
439		'Р'   => array('0', '9', '9', '9'),
440		'РЖ'  => array('0', '4', '4', '4'),
441		'С'   => array('0', '4', '4', '4'),
442		'Т'   => array('0', '3', '3', '3'),
443		'У'   => array('1', '0', '', ''),
444		'Ф'   => array('0', '7', '7', '7'),
445		'Х'   => array('0', '5', '5', '5'),
446		'Ц'   => array('0', '4', '4', '4'),
447		'Ч'   => array('0', '4', '4', '4'),
448		'Ш'   => array('0', '4', '4', '4'),
449		'Щ'   => array('0', '2', '4', '4'),
450		'Ъ'   => array('0', '', '', ''),
451		'Ы'   => array('0', '1', '', ''),
452		'Ь'   => array('0', '', '', ''),
453		'Э'   => array('1', '0', '', ''),
454		'Ю'   => array('0', '1', '', ''),
455		'Я'   => array('0', '1', '', ''),
456		// Greek alphabet
457		'Α'   => array('1', '0', '', ''),
458		'Ά'   => array('1', '0', '', ''),
459		'ΑΙ'  => array('1', '0', '1', ''),
460		'ΑΥ'  => array('1', '0', '1', ''),
461		'Β'   => array('0', '7', '7', '7'),
462		'Γ'   => array('0', '5', '5', '5'),
463		'Δ'   => array('0', '3', '3', '3'),
464		'Ε'   => array('1', '0', '', ''),
465		'Έ'   => array('1', '0', '', ''),
466		'ΕΙ'  => array('1', '0', '1', ''),
467		'ΕΥ'  => array('1', '1', '1', ''),
468		'Ζ'   => array('0', '4', '4', '4'),
469		'Η'   => array('1', '0', '', ''),
470		'Ή'   => array('1', '0', '', ''),
471		'Θ'   => array('0', '3', '3', '3'),
472		'Ι'   => array('1', '0', '', ''),
473		'Ί'   => array('1', '0', '', ''),
474		'Ϊ'   => array('1', '0', '', ''),
475		'ΐ'   => array('1', '0', '', ''),
476		'Κ'   => array('0', '5', '5', '5'),
477		'Λ'   => array('0', '8', '8', '8'),
478		'Μ'   => array('0', '6', '6', '6'),
479		'ΜΠ'  => array('0', '7', '7', '7'),
480		'Ν'   => array('0', '6', '6', '6'),
481		'ΝΤ'  => array('0', '3', '3', '3'),
482		'Ξ'   => array('0', '5', '54', '54'),
483		'Ο'   => array('1', '0', '', ''),
484		'Ό'   => array('1', '0', '', ''),
485		'ΟΙ'  => array('1', '0', '1', ''),
486		'ΟΥ'  => array('1', '0', '1', ''),
487		'Π'   => array('0', '7', '7', '7'),
488		'Ρ'   => array('0', '9', '9', '9'),
489		'Σ'   => array('0', '4', '4', '4'),
490		'ς'   => array('0', '', '', '4'),
491		'Τ'   => array('0', '3', '3', '3'),
492		'ΤΖ'  => array('0', '4', '4', '4'),
493		'ΤΣ'  => array('0', '4', '4', '4'),
494		'Υ'   => array('1', '1', '', ''),
495		'Ύ'   => array('1', '1', '', ''),
496		'Ϋ'   => array('1', '1', '', ''),
497		'ΰ'   => array('1', '1', '', ''),
498		'ΥΚ'  => array('1', '5', '5', '5'),
499		'ΥΥ'  => array('1', '65', '65', '65'),
500		'Φ'   => array('0', '7', '7', '7'),
501		'Χ'   => array('0', '5', '5', '5'),
502		'Ψ'   => array('0', '7', '7', '7'),
503		'Ω'   => array('1', '0', '', ''),
504		'Ώ'   => array('1', '0', '', ''),
505		// Hebrew alphabet
506		'א'     => array('1', '0', '', ''),
507		'או'    => array('1', '0', '7', ''),
508		'אג'    => array('1', '4', '4', '4', '5', '5', '5', '34', '34', '34'),
509		'בב'    => array('0', '7', '7', '7', '77', '77', '77'),
510		'ב'     => array('0', '7', '7', '7'),
511		'גג'    => array('0', '4', '4', '4', '5', '5', '5', '45', '45', '45', '55', '55', '55', '54', '54', '54'),
512		'גד'    => array('0', '43', '43', '43', '53', '53', '53'),
513		'גה'    => array('0', '45', '45', '45', '55', '55', '55'),
514		'גז'    => array('0', '44', '44', '44', '45', '45', '45'),
515		'גח'    => array('0', '45', '45', '45', '55', '55', '55'),
516		'גכ'    => array('0', '45', '45', '45', '55', '55', '55'),
517		'גך'    => array('0', '45', '45', '45', '55', '55', '55'),
518		'גצ'    => array('0', '44', '44', '44', '45', '45', '45'),
519		'גץ'    => array('0', '44', '44', '44', '45', '45', '45'),
520		'גק'    => array('0', '45', '45', '45', '54', '54', '54'),
521		'גש'    => array('0', '44', '44', '44', '54', '54', '54'),
522		'גת'    => array('0', '43', '43', '43', '53', '53', '53'),
523		'ג'     => array('0', '4', '4', '4', '5', '5', '5'),
524		'דז'    => array('0', '4', '4', '4'),
525		'דד'    => array('0', '3', '3', '3', '33', '33', '33'),
526		'דט'    => array('0', '33', '33', '33'),
527		'דש'    => array('0', '4', '4', '4'),
528		'דצ'    => array('0', '4', '4', '4'),
529		'דץ'    => array('0', '4', '4', '4'),
530		'ד'     => array('0', '3', '3', '3'),
531		'הג'    => array('0', '54', '54', '54', '55', '55', '55'),
532		'הכ'    => array('0', '55', '55', '55'),
533		'הח'    => array('0', '55', '55', '55'),
534		'הק'    => array('0', '55', '55', '55', '5', '5', '5'),
535		'הה'    => array('0', '5', '5', '', '55', '55', ''),
536		'ה'     => array('0', '5', '5', ''),
537		'וי'    => array('1', '', '', '', '7', '7', '7'),
538		'ו'     => array('1', '7', '7', '7', '7', '', ''),
539		'וו'    => array('1', '7', '7', '7', '7', '', ''),
540		'וופ'   => array('1', '7', '7', '7', '77', '77', '77'),
541		'זש'    => array('0', '4', '4', '4', '44', '44', '44'),
542		'זדז'   => array('0', '2', '4', '4'),
543		'ז'     => array('0', '4', '4', '4'),
544		'זג'    => array('0', '44', '44', '44', '45', '45', '45'),
545		'זז'    => array('0', '4', '4', '4', '44', '44', '44'),
546		'זס'    => array('0', '44', '44', '44'),
547		'זצ'    => array('0', '44', '44', '44'),
548		'זץ'    => array('0', '44', '44', '44'),
549		'חג'    => array('0', '54', '54', '54', '53', '53', '53'),
550		'חח'    => array('0', '5', '5', '5', '55', '55', '55'),
551		'חק'    => array('0', '55', '55', '55', '5', '5', '5'),
552		'חכ'    => array('0', '45', '45', '45', '55', '55', '55'),
553		'חס'    => array('0', '5', '54', '54'),
554		'חש'    => array('0', '5', '54', '54'),
555		'ח'     => array('0', '5', '5', '5'),
556		'טש'    => array('0', '4', '4', '4'),
557		'טד'    => array('0', '33', '33', '33'),
558		'טי'    => array('0', '3', '3', '3', '4', '4', '4', '3', '3', '34'),
559		'טת'    => array('0', '33', '33', '33'),
560		'טט'    => array('0', '3', '3', '3', '33', '33', '33'),
561		'ט'     => array('0', '3', '3', '3'),
562		'י'     => array('1', '1', '', ''),
563		'יא'    => array('1', '1', '', '', '1', '1', '1'),
564		'כג'    => array('0', '55', '55', '55', '54', '54', '54'),
565		'כש'    => array('0', '5', '54', '54'),
566		'כס'    => array('0', '5', '54', '54'),
567		'ככ'    => array('0', '5', '5', '5', '55', '55', '55'),
568		'כך'    => array('0', '5', '5', '5', '55', '55', '55'),
569		'כ'     => array('0', '5', '5', '5'),
570		'כח'    => array('0', '55', '55', '55', '5', '5', '5'),
571		'ך'     => array('0', '', '5', '5'),
572		'ל'     => array('0', '8', '8', '8'),
573		'לל'    => array('0', '88', '88', '88', '8', '8', '8'),
574		'מנ'    => array('0', '66', '66', '66'),
575		'מן'    => array('0', '66', '66', '66'),
576		'ממ'    => array('0', '6', '6', '6', '66', '66', '66'),
577		'מם'    => array('0', '6', '6', '6', '66', '66', '66'),
578		'מ'     => array('0', '6', '6', '6'),
579		'ם'     => array('0', '', '6', '6'),
580		'נמ'    => array('0', '66', '66', '66'),
581		'נם'    => array('0', '66', '66', '66'),
582		'ננ'    => array('0', '6', '6', '6', '66', '66', '66'),
583		'נן'    => array('0', '6', '6', '6', '66', '66', '66'),
584		'נ'     => array('0', '6', '6', '6'),
585		'ן'     => array('0', '', '6', '6'),
586		'סתש'   => array('0', '2', '4', '4'),
587		'סתז'   => array('0', '2', '4', '4'),
588		'סטז'   => array('0', '2', '4', '4'),
589		'סטש'   => array('0', '2', '4', '4'),
590		'סצד'   => array('0', '2', '4', '4'),
591		'סט'    => array('0', '2', '4', '4', '43', '43', '43'),
592		'סת'    => array('0', '2', '4', '4', '43', '43', '43'),
593		'סג'    => array('0', '44', '44', '44', '4', '4', '4'),
594		'סס'    => array('0', '4', '4', '4', '44', '44', '44'),
595		'סצ'    => array('0', '44', '44', '44'),
596		'סץ'    => array('0', '44', '44', '44'),
597		'סז'    => array('0', '44', '44', '44'),
598		'סש'    => array('0', '44', '44', '44'),
599		'ס'     => array('0', '4', '4', '4'),
600		'ע'     => array('1', '0', '', ''),
601		'פב'    => array('0', '7', '7', '7', '77', '77', '77'),
602		'פוו'   => array('0', '7', '7', '7', '77', '77', '77'),
603		'פפ'    => array('0', '7', '7', '7', '77', '77', '77'),
604		'פף'    => array('0', '7', '7', '7', '77', '77', '77'),
605		'פ'     => array('0', '7', '7', '7'),
606		'ף'     => array('0', '', '7', '7'),
607		'צג'    => array('0', '44', '44', '44', '45', '45', '45'),
608		'צז'    => array('0', '44', '44', '44'),
609		'צס'    => array('0', '44', '44', '44'),
610		'צצ'    => array('0', '4', '4', '4', '5', '5', '5', '44', '44', '44', '54', '54', '54', '45', '45', '45'),
611		'צץ'    => array('0', '4', '4', '4', '5', '5', '5', '44', '44', '44', '54', '54', '54'),
612		'צש'    => array('0', '44', '44', '44', '4', '4', '4', '5', '5', '5'),
613		'צ'     => array('0', '4', '4', '4', '5', '5', '5'),
614		'ץ'     => array('0', '', '4', '4'),
615		'קה'    => array('0', '55', '55', '5'),
616		'קס'    => array('0', '5', '54', '54'),
617		'קש'    => array('0', '5', '54', '54'),
618		'קק'    => array('0', '5', '5', '5', '55', '55', '55'),
619		'קח'    => array('0', '55', '55', '55'),
620		'קכ'    => array('0', '55', '55', '55'),
621		'קך'    => array('0', '55', '55', '55'),
622		'קג'    => array('0', '55', '55', '55', '54', '54', '54'),
623		'ק'     => array('0', '5', '5', '5'),
624		'רר'    => array('0', '99', '99', '99', '9', '9', '9'),
625		'ר'     => array('0', '9', '9', '9'),
626		'שטז'   => array('0', '2', '4', '4'),
627		'שתש'   => array('0', '2', '4', '4'),
628		'שתז'   => array('0', '2', '4', '4'),
629		'שטש'   => array('0', '2', '4', '4'),
630		'שד'    => array('0', '2', '43', '43'),
631		'שז'    => array('0', '44', '44', '44'),
632		'שס'    => array('0', '44', '44', '44'),
633		'שת'    => array('0', '2', '43', '43'),
634		'שג'    => array('0', '4', '4', '4', '44', '44', '44', '4', '43', '43'),
635		'שט'    => array('0', '2', '43', '43', '44', '44', '44'),
636		'שצ'    => array('0', '44', '44', '44', '45', '45', '45'),
637		'שץ'    => array('0', '44', '', '44', '45', '', '45'),
638		'שש'    => array('0', '4', '4', '4', '44', '44', '44'),
639		'ש'     => array('0', '4', '4', '4'),
640		'תג'    => array('0', '34', '34', '34'),
641		'תז'    => array('0', '34', '34', '34'),
642		'תש'    => array('0', '4', '4', '4'),
643		'תת'    => array('0', '3', '3', '3', '4', '4', '4', '33', '33', '33', '44', '44', '44', '34', '34', '34', '43', '43', '43'),
644		'ת'     => array('0', '3', '3', '3', '4', '4', '4'),
645		// Arabic alphabet
646		'ا'   => array('1', '0', '', ''),
647		'ب'   => array('0', '7', '7', '7'),
648		'ت'   => array('0', '3', '3', '3'),
649		'ث'   => array('0', '3', '3', '3'),
650		'ج'   => array('0', '4', '4', '4'),
651		'ح'   => array('0', '5', '5', '5'),
652		'خ'   => array('0', '5', '5', '5'),
653		'د'   => array('0', '3', '3', '3'),
654		'ذ'   => array('0', '3', '3', '3'),
655		'ر'   => array('0', '9', '9', '9'),
656		'ز'   => array('0', '4', '4', '4'),
657		'س'   => array('0', '4', '4', '4'),
658		'ش'   => array('0', '4', '4', '4'),
659		'ص'   => array('0', '4', '4', '4'),
660		'ض'   => array('0', '3', '3', '3'),
661		'ط'   => array('0', '3', '3', '3'),
662		'ظ'   => array('0', '4', '4', '4'),
663		'ع'   => array('1', '0', '', ''),
664		'غ'   => array('0', '0', '', ''),
665		'ف'   => array('0', '7', '7', '7'),
666		'ق'   => array('0', '5', '5', '5'),
667		'ك'   => array('0', '5', '5', '5'),
668		'ل'   => array('0', '8', '8', '8'),
669		'لا'  => array('0', '8', '8', '8'),
670		'م'   => array('0', '6', '6', '6'),
671		'ن'   => array('0', '6', '6', '6'),
672		'هن'  => array('0', '66', '66', '66'),
673		'ه'   => array('0', '5', '5', ''),
674		'و'   => array('1', '', '', '', '7', '', ''),
675		'ي'   => array('0', '1', '', ''),
676		'آ'   => array('0', '1', '', ''),
677		'ة'   => array('0', '', '', '3'),
678		'ی'   => array('0', '1', '', ''),
679		'ى'   => array('1', '1', '', ''),
680	);
681
682	/**
683	 * Calculate the Daitch-Mokotoff soundex for a word.
684	 *
685	 * @param string $name
686	 *
687	 * @return string[] List of possible DM codes for the word.
688	 */
689	private static function daitchMokotoffWord($name) {
690		// Apply special transformation rules to the input string
691		$name = I18N::strtoupper($name);
692		foreach (self::$transformNameTable as $transformRule) {
693			$name = str_replace($transformRule[0], $transformRule[1], $name);
694		}
695
696		// Initialize
697		$name_script = I18N::textScript($name);
698		$noVowels    = ($name_script == 'Hebr' || $name_script == 'Arab');
699
700		$lastPos         = strlen($name) - 1;
701		$currPos         = 0;
702		$state           = 1; // 1: start of input string, 2: before vowel, 3: other
703		$result          = array(); // accumulate complete 6-digit D-M codes here
704		$partialResult   = array(); // accumulate incomplete D-M codes here
705		$partialResult[] = array('!'); // initialize 1st partial result  ('!' stops "duplicate sound" check)
706
707		// Loop through the input string.
708		// Stop when the string is exhausted or when no more partial results remain
709		while (count($partialResult) !== 0 && $currPos <= $lastPos) {
710			// Find the DM coding table entry for the chunk at the current position
711			$thisEntry = substr($name, $currPos, self::MAXCHAR); // Get maximum length chunk
712			while ($thisEntry != '') {
713				if (isset(self::$dmsounds[$thisEntry])) {
714					break;
715				}
716				$thisEntry = substr($thisEntry, 0, -1); // Not in table: try a shorter chunk
717			}
718			if ($thisEntry === '') {
719				$currPos++; // Not in table: advance pointer to next byte
720				continue; // and try again
721			}
722
723			$soundTableEntry = self::$dmsounds[$thisEntry];
724			$workingResult   = $partialResult;
725			$partialResult   = array();
726			$currPos += strlen($thisEntry);
727
728			// Not at beginning of input string
729			if ($state != 1) {
730				if ($currPos <= $lastPos) {
731					// Determine whether the next chunk is a vowel
732					$nextEntry = substr($name, $currPos, self::MAXCHAR); // Get maximum length chunk
733					while ($nextEntry != '') {
734						if (isset(self::$dmsounds[$nextEntry])) {
735							break;
736						}
737						$nextEntry = substr($nextEntry, 0, -1); // Not in table: try a shorter chunk
738					}
739				} else {
740					$nextEntry = '';
741				}
742				if ($nextEntry != '' && self::$dmsounds[$nextEntry][0] != '0') {
743					$state = 2;
744				} else {
745					// Next chunk is a vowel
746					$state = 3;
747				}
748			}
749
750			while ($state < count($soundTableEntry)) {
751				// empty means 'ignore this sound in this state'
752				if ($soundTableEntry[$state] == '') {
753					foreach ($workingResult as $workingEntry) {
754						$tempEntry = $workingEntry;
755						$tempEntry[count($tempEntry) - 1] .= '!'; // Prevent false 'doubles'
756						$partialResult[] = $tempEntry;
757					}
758				} else {
759					foreach ($workingResult as $workingEntry) {
760						if ($soundTableEntry[$state] !== $workingEntry[count($workingEntry) - 1]) {
761							// Incoming sound isn't a duplicate of the previous sound
762							$workingEntry[] = $soundTableEntry[$state];
763						} else {
764							// Incoming sound is a duplicate of the previous sound
765							// For Hebrew and Arabic, we need to create a pair of D-M sound codes,
766							// one of the pair with only a single occurrence of the duplicate sound,
767							// the other with both occurrences
768							if ($noVowels) {
769								$workingEntry[] = $soundTableEntry[$state];
770							}
771						}
772						if (count($workingEntry) < 7) {
773							$partialResult[] = $workingEntry;
774						} else {
775							// This is the 6th code in the sequence
776							// We're looking for 7 entries because the first is '!' and doesn't count
777							$tempResult = str_replace('!', '', implode('', $workingEntry));
778							// Only return codes from recognisable sounds
779							if ($tempResult) {
780								$result[] = substr($tempResult . '000000', 0, 6);
781							}
782						}
783					}
784				}
785				$state = $state + 3; // Advance to next triplet while keeping the same basic state
786			}
787		}
788
789		// Zero-fill and copy all remaining partial results
790		foreach ($partialResult as $workingEntry) {
791			$tempResult = str_replace('!', '', implode('', $workingEntry));
792			// Only return codes from recognisable sounds
793			if ($tempResult) {
794				$result[] = substr($tempResult . '000000', 0, 6);
795			}
796		}
797
798		return $result;
799	}
800}
801