]> git.openstreetmap.org Git - nominatim.git/blob - lib-php/TokenList.php
avoid multi-term partials in names
[nominatim.git] / lib-php / TokenList.php
1 <?php
2
3 namespace Nominatim;
4
5 require_once(CONST_LibDir.'/TokenCountry.php');
6 require_once(CONST_LibDir.'/TokenHousenumber.php');
7 require_once(CONST_LibDir.'/TokenPostcode.php');
8 require_once(CONST_LibDir.'/TokenSpecialTerm.php');
9 require_once(CONST_LibDir.'/TokenWord.php');
10 require_once(CONST_LibDir.'/SpecialSearchOperator.php');
11
12 /**
13  * Saves information about the tokens that appear in a search query.
14  *
15  * Tokens are sorted by their normalized form, the token word. There are different
16  * kinds of tokens, represented by different Token* classes. Note that
17  * tokens do not have a common base class. All tokens need to have a field
18  * with the word id that points to an entry in the `word` database table
19  * but otherwise the information saved about a token can be very different.
20  *
21  * There are two different kinds of token words: full words and partial terms.
22  *
23  * Full words start with a space. They represent a complete name of a place.
24  * All special tokens are normally full words.
25  *
26  * Partial terms have no space at the beginning. They may represent a part of
27  * a name of a place (e.g. in the name 'World Trade Center' a partial term
28  * would be 'Trade' or 'Trade Center'). They are only used in TokenWord.
29  */
30 class TokenList
31 {
32     // List of list of tokens indexed by their word_token.
33     private $aTokens = array();
34
35
36     /**
37      * Return total number of tokens.
38      *
39      * @return Integer
40      */
41     public function count()
42     {
43         return count($this->aTokens);
44     }
45
46     /**
47      * Check if there are tokens for the given token word.
48      *
49      * @param string $sWord Token word to look for.
50      *
51      * @return bool True if there is one or more token for the token word.
52      */
53     public function contains($sWord)
54     {
55         return isset($this->aTokens[$sWord]);
56     }
57
58     /**
59      * Check if there are partial or full tokens for the given word.
60      *
61      * @param string $sWord Token word to look for.
62      *
63      * @return bool True if there is one or more token for the token word.
64      */
65     public function containsAny($sWord)
66     {
67         return isset($this->aTokens[$sWord]) || isset($this->aTokens[' '.$sWord]);
68     }
69
70     /**
71      * Get the list of tokens for the given token word.
72      *
73      * @param string $sWord Token word to look for.
74      *
75      * @return object[] Array of tokens for the given token word or an
76      *                  empty array if no tokens could be found.
77      */
78     public function get($sWord)
79     {
80         return isset($this->aTokens[$sWord]) ? $this->aTokens[$sWord] : array();
81     }
82
83     public function getFullWordIDs()
84     {
85         $ids = array();
86
87         foreach ($this->aTokens as $aTokenList) {
88             foreach ($aTokenList as $oToken) {
89                 if (is_a($oToken, '\Nominatim\Token\Word') && !$oToken->bPartial) {
90                     $ids[$oToken->iId] = $oToken->iId;
91                 }
92             }
93         }
94
95         return $ids;
96     }
97
98     /**
99      * Add token information from the word table in the database.
100      *
101      * @param object   $oDB           Nominatim::DB instance.
102      * @param string[] $aTokens       List of tokens to look up in the database.
103      * @param string[] $aCountryCodes List of country restrictions.
104      * @param string   $sNormQuery    Normalized query string.
105      * @param object   $oNormalizer   Normalizer function to use on tokens.
106      *
107      * @return void
108      */
109     public function addTokensFromDB(&$oDB, &$aTokens, &$aCountryCodes, $sNormQuery, $oNormalizer)
110     {
111         // Check which tokens we have, get the ID numbers
112         $sSQL = 'SELECT word_id, word_token, word, class, type, country_code,';
113         $sSQL .= ' operator, coalesce(search_name_count, 0) as count';
114         $sSQL .= ' FROM word WHERE word_token in (';
115         $sSQL .= join(',', $oDB->getDBQuotedList($aTokens)).')';
116
117         Debug::printSQL($sSQL);
118
119         $aDBWords = $oDB->getAll($sSQL, null, 'Could not get word tokens.');
120
121         foreach ($aDBWords as $aWord) {
122             $oToken = null;
123             $iId = (int) $aWord['word_id'];
124
125             if ($aWord['class']) {
126                 // Special terms need to appear in their normalized form.
127                 if ($aWord['word']) {
128                     $sNormWord = $aWord['word'];
129                     if ($oNormalizer != null) {
130                         $sNormWord = $oNormalizer->transliterate($aWord['word']);
131                     }
132                     if (strpos($sNormQuery, $sNormWord) === false) {
133                         continue;
134                     }
135                 }
136
137                 if ($aWord['class'] == 'place' && $aWord['type'] == 'house') {
138                     $oToken = new Token\HouseNumber($iId, trim($aWord['word_token']));
139                 } elseif ($aWord['class'] == 'place' && $aWord['type'] == 'postcode') {
140                     if ($aWord['word']
141                         && pg_escape_string($aWord['word']) == $aWord['word']
142                     ) {
143                         $oToken = new Token\Postcode(
144                             $iId,
145                             $aWord['word'],
146                             $aWord['country_code']
147                         );
148                     }
149                 } else {
150                     // near and in operator the same at the moment
151                     $oToken = new Token\SpecialTerm(
152                         $iId,
153                         $aWord['class'],
154                         $aWord['type'],
155                         $aWord['operator'] ? Operator::NEAR : Operator::NONE
156                     );
157                 }
158             } elseif ($aWord['country_code']) {
159                 // Filter country tokens that do not match restricted countries.
160                 if (!$aCountryCodes
161                     || in_array($aWord['country_code'], $aCountryCodes)
162                 ) {
163                     $oToken = new Token\Country($iId, $aWord['country_code']);
164                 }
165             } else {
166                 $oToken = new Token\Word(
167                     $iId,
168                     $aWord['word_token'][0] != ' ',
169                     (int) $aWord['count'],
170                     substr_count($aWord['word_token'], ' ')
171                 );
172             }
173
174             if ($oToken) {
175                 $this->addToken($aWord['word_token'], $oToken);
176             }
177         }
178     }
179
180     /**
181      * Add a new token for the given word.
182      *
183      * @param string $sWord  Word the token describes.
184      * @param object $oToken Token object to add.
185      *
186      * @return void
187      */
188     public function addToken($sWord, $oToken)
189     {
190         if (isset($this->aTokens[$sWord])) {
191             $this->aTokens[$sWord][] = $oToken;
192         } else {
193             $this->aTokens[$sWord] = array($oToken);
194         }
195     }
196
197     public function debugTokenByWordIdList()
198     {
199         $aWordsIDs = array();
200         foreach ($this->aTokens as $sToken => $aWords) {
201             foreach ($aWords as $aToken) {
202                 if ($aToken->iId !== null) {
203                     $aWordsIDs[$aToken->iId] =
204                         '#'.$sToken.'('.$aToken->iId.')#';
205                 }
206             }
207         }
208
209         return $aWordsIDs;
210     }
211
212     public function debugInfo()
213     {
214         return $this->aTokens;
215     }
216 }