]> git.openstreetmap.org Git - nominatim.git/blob - lib/Geocode.php
make sure place exists before printing hierarchy
[nominatim.git] / lib / Geocode.php
1 <?php
2         class Geocode
3         {
4                 protected $oDB;
5
6                 protected $aLangPrefOrder = array();
7
8                 protected $bIncludeAddressDetails = false;
9
10                 protected $bIncludePolygonAsPoints = false;
11                 protected $bIncludePolygonAsText = false;
12                 protected $bIncludePolygonAsGeoJSON = false;
13                 protected $bIncludePolygonAsKML = false;
14                 protected $bIncludePolygonAsSVG = false;
15
16                 protected $aExcludePlaceIDs = array();
17                 protected $bDeDupe = true;
18                 protected $bReverseInPlan = false;
19
20                 protected $iLimit = 20;
21                 protected $iFinalLimit = 10;
22                 protected $iOffset = 0;
23                 protected $bFallback = false;
24
25                 protected $aCountryCodes = false;
26                 protected $aNearPoint = false;
27
28                 protected $bBoundedSearch = false;
29                 protected $aViewBox = false;
30                 protected $sViewboxSmallSQL = false;
31                 protected $sViewboxLargeSQL = false;
32                 protected $aRoutePoints = false;
33
34                 protected $iMaxRank = 20;
35                 protected $iMinAddressRank = 0;
36                 protected $iMaxAddressRank = 30;
37                 protected $aAddressRankList = array();
38                 protected $exactMatchCache = array();
39
40                 protected $sAllowedTypesSQLList = false;
41
42                 protected $sQuery = false;
43                 protected $aStructuredQuery = false;
44
45                 function Geocode(&$oDB)
46                 {
47                         $this->oDB =& $oDB;
48                 }
49
50                 function setReverseInPlan($bReverse)
51                 {
52                         $this->bReverseInPlan = $bReverse;
53                 }
54
55                 function setLanguagePreference($aLangPref)
56                 {
57                         $this->aLangPrefOrder = $aLangPref;
58                 }
59
60                 function setIncludeAddressDetails($bAddressDetails = true)
61                 {
62                         $this->bIncludeAddressDetails = (bool)$bAddressDetails;
63                 }
64
65                 function getIncludeAddressDetails()
66                 {
67                         return $this->bIncludeAddressDetails;
68                 }
69
70                 function setIncludePolygonAsPoints($b = true)
71                 {
72                         $this->bIncludePolygonAsPoints = $b;
73                 }
74
75                 function getIncludePolygonAsPoints()
76                 {
77                         return $this->bIncludePolygonAsPoints;
78                 }
79
80                 function setIncludePolygonAsText($b = true)
81                 {
82                         $this->bIncludePolygonAsText = $b;
83                 }
84
85                 function getIncludePolygonAsText()
86                 {
87                         return $this->bIncludePolygonAsText;
88                 }
89
90                 function setIncludePolygonAsGeoJSON($b = true)
91                 {
92                         $this->bIncludePolygonAsGeoJSON = $b;
93                 }
94
95                 function setIncludePolygonAsKML($b = true)
96                 {
97                         $this->bIncludePolygonAsKML = $b;
98                 }
99
100                 function setIncludePolygonAsSVG($b = true)
101                 {
102                         $this->bIncludePolygonAsSVG = $b;
103                 }
104
105                 function setDeDupe($bDeDupe = true)
106                 {
107                         $this->bDeDupe = (bool)$bDeDupe;
108                 }
109
110                 function setLimit($iLimit = 10)
111                 {
112                         if ($iLimit > 50) $iLimit = 50;
113                         if ($iLimit < 1) $iLimit = 1;
114
115                         $this->iFinalLimit = $iLimit;
116                         $this->iLimit = $this->iFinalLimit + min($this->iFinalLimit, 10);
117                 }
118
119                 function setOffset($iOffset = 0)
120                 {
121                         $this->iOffset = $iOffset;
122                 }
123
124                 function setFallback($bFallback = true)
125                 {
126                         $this->bFallback = (bool)$bFallback;
127                 }
128
129                 function setExcludedPlaceIDs($a)
130                 {
131                         // TODO: force to int
132                         $this->aExcludePlaceIDs = $a;
133                 }
134
135                 function getExcludedPlaceIDs()
136                 {
137                         return $this->aExcludePlaceIDs;
138                 }
139
140                 function setBounded($bBoundedSearch = true)
141                 {
142                         $this->bBoundedSearch = (bool)$bBoundedSearch;
143                 }
144
145                 function setViewBox($fLeft, $fBottom, $fRight, $fTop)
146                 {
147                         $this->aViewBox = array($fLeft, $fBottom, $fRight, $fTop);
148                 }
149
150                 function getViewBoxString()
151                 {
152                         if (!$this->aViewBox) return null;
153                         return $this->aViewBox[0].','.$this->aViewBox[3].','.$this->aViewBox[2].','.$this->aViewBox[1];
154                 }
155
156                 function setRoute($aRoutePoints)
157                 {
158                         $this->aRoutePoints = $aRoutePoints;
159                 }
160
161                 function setFeatureType($sFeatureType)
162                 {
163                         switch($sFeatureType)
164                         {
165                         case 'country':
166                                 $this->setRankRange(4, 4);
167                                 break;
168                         case 'state':
169                                 $this->setRankRange(8, 8);
170                                 break;
171                         case 'city':
172                                 $this->setRankRange(14, 16);
173                                 break;
174                         case 'settlement':
175                                 $this->setRankRange(8, 20);
176                                 break;
177                         }
178                 }
179
180                 function setRankRange($iMin, $iMax)
181                 {
182                         $this->iMinAddressRank = (int)$iMin;
183                         $this->iMaxAddressRank = (int)$iMax;
184                 }
185
186                 function setNearPoint($aNearPoint, $fRadiusDeg = 0.1)
187                 {
188                         $this->aNearPoint = array((float)$aNearPoint[0], (float)$aNearPoint[1], (float)$fRadiusDeg);
189                 }
190
191                 function setCountryCodesList($aCountryCodes)
192                 {
193                         $this->aCountryCodes = $aCountryCodes;
194                 }
195
196                 function setQuery($sQueryString)
197                 {
198                         $this->sQuery = $sQueryString;
199                         $this->aStructuredQuery = false;
200                 }
201
202                 function getQueryString()
203                 {
204                         return $this->sQuery;
205                 }
206
207
208                 function loadParamArray($aParams)
209                 {
210                         if (isset($aParams['addressdetails'])) $this->bIncludeAddressDetails = (bool)$aParams['addressdetails'];
211                         if (isset($aParams['bounded'])) $this->bBoundedSearch = (bool)$aParams['bounded'];
212                         if (isset($aParams['dedupe'])) $this->bDeDupe = (bool)$aParams['dedupe'];
213
214                         if (isset($aParams['limit'])) $this->setLimit((int)$aParams['limit']);
215                         if (isset($aParams['offset'])) $this->iOffset = (int)$aParams['offset'];
216
217                         if (isset($aParams['fallback'])) $this->bFallback = (bool)$aParams['fallback'];
218
219                         // List of excluded Place IDs - used for more acurate pageing
220                         if (isset($aParams['exclude_place_ids']) && $aParams['exclude_place_ids'])
221                         {
222                                 foreach(explode(',',$aParams['exclude_place_ids']) as $iExcludedPlaceID)
223                                 {
224                                         $iExcludedPlaceID = (int)$iExcludedPlaceID;
225                                         if ($iExcludedPlaceID)
226                                                 $aExcludePlaceIDs[$iExcludedPlaceID] = $iExcludedPlaceID;
227                                 }
228
229                                 if (isset($aExcludePlaceIDs))
230                                         $this->aExcludePlaceIDs = $aExcludePlaceIDs;
231                         }
232
233                         // Only certain ranks of feature
234                         if (isset($aParams['featureType'])) $this->setFeatureType($aParams['featureType']);
235                         if (isset($aParams['featuretype'])) $this->setFeatureType($aParams['featuretype']);
236
237                         // Country code list
238                         if (isset($aParams['countrycodes']))
239                         {
240                                 $aCountryCodes = array();
241                                 foreach(explode(',',$aParams['countrycodes']) as $sCountryCode)
242                                 {
243                                         if (preg_match('/^[a-zA-Z][a-zA-Z]$/', $sCountryCode))
244                                         {
245                                                 $aCountryCodes[] = strtolower($sCountryCode);
246                                         }
247                                 }
248                                 $this->aCountryCodes = $aCountryCodes;
249                         }
250
251                         if (isset($aParams['viewboxlbrt']) && $aParams['viewboxlbrt'])
252                         {
253                                 $aCoOrdinatesLBRT = explode(',',$aParams['viewboxlbrt']);
254                                 $this->setViewBox($aCoOrdinatesLBRT[0], $aCoOrdinatesLBRT[1], $aCoOrdinatesLBRT[2], $aCoOrdinatesLBRT[3]);
255                         }
256                         else if (isset($aParams['viewbox']) && $aParams['viewbox'])
257                         {
258                                 $aCoOrdinatesLTRB = explode(',',$aParams['viewbox']);
259                                 $this->setViewBox($aCoOrdinatesLTRB[0], $aCoOrdinatesLTRB[3], $aCoOrdinatesLTRB[2], $aCoOrdinatesLTRB[1]);
260                         }
261
262                         if (isset($aParams['route']) && $aParams['route'] && isset($aParams['routewidth']) && $aParams['routewidth'])
263                         {
264                                 $aPoints = explode(',',$aParams['route']);
265                                 if (sizeof($aPoints) % 2 != 0)
266                                 {
267                                         userError("Uneven number of points");
268                                         exit;
269                                 }
270                                 $fPrevCoord = false;
271                                 $aRoute = array();
272                                 foreach($aPoints as $i => $fPoint)
273                                 {
274                                         if ($i%2)
275                                         {
276                                                 $aRoute[] = array((float)$fPoint, $fPrevCoord);
277                                         }
278                                         else
279                                         {
280                                                 $fPrevCoord = (float)$fPoint;
281                                         }
282                                 }
283                                 $this->aRoutePoints = $aRoute;
284                         }
285                 }
286
287                 function setQueryFromParams($aParams)
288                 {
289                         // Search query
290                         $sQuery = (isset($aParams['q'])?trim($aParams['q']):'');
291                         if (!$sQuery)
292                         {
293                                 $this->setStructuredQuery(@$aParams['amenity'], @$aParams['street'], @$aParams['city'], @$aParams['county'], @$aParams['state'], @$aParams['country'], @$aParams['postalcode']);
294                                 $this->setReverseInPlan(false);
295                         }
296                         else
297                         {
298                                 $this->setQuery($sQuery);
299                         }
300                 }
301
302                 function loadStructuredAddressElement($sValue, $sKey, $iNewMinAddressRank, $iNewMaxAddressRank, $aItemListValues)
303                 {
304                         $sValue = trim($sValue);
305                         if (!$sValue) return false;
306                         $this->aStructuredQuery[$sKey] = $sValue;
307                         if ($this->iMinAddressRank == 0 && $this->iMaxAddressRank == 30)
308                         {
309                                 $this->iMinAddressRank = $iNewMinAddressRank;
310                                 $this->iMaxAddressRank = $iNewMaxAddressRank;
311                         }
312                         if ($aItemListValues) $this->aAddressRankList = array_merge($this->aAddressRankList, $aItemListValues);
313                         return true;
314                 }
315
316                 function setStructuredQuery($sAmentiy = false, $sStreet = false, $sCity = false, $sCounty = false, $sState = false, $sCountry = false, $sPostalCode = false)
317                 {
318                         $this->sQuery = false;
319
320                         // Reset
321                         $this->iMinAddressRank = 0;
322                         $this->iMaxAddressRank = 30;
323                         $this->aAddressRankList = array();
324
325                         $this->aStructuredQuery = array();
326                         $this->sAllowedTypesSQLList = '';
327
328                         $this->loadStructuredAddressElement($sAmentiy, 'amenity', 26, 30, false);
329                         $this->loadStructuredAddressElement($sStreet, 'street', 26, 30, false);
330                         $this->loadStructuredAddressElement($sCity, 'city', 14, 24, false);
331                         $this->loadStructuredAddressElement($sCounty, 'county', 9, 13, false);
332                         $this->loadStructuredAddressElement($sState, 'state', 8, 8, false);
333                         $this->loadStructuredAddressElement($sPostalCode, 'postalcode' , 5, 11, array(5, 11));
334                         $this->loadStructuredAddressElement($sCountry, 'country', 4, 4, false);
335
336                         if (sizeof($this->aStructuredQuery) > 0) 
337                         {
338                                 $this->sQuery = join(', ', $this->aStructuredQuery);
339                                 if ($this->iMaxAddressRank < 30)
340                                 {
341                                         $sAllowedTypesSQLList = '(\'place\',\'boundary\')';
342                                 }
343                         }
344                 }
345
346                 function fallbackStructuredQuery()
347                 {
348                         if (!$this->aStructuredQuery) return false;
349
350                         $aParams = $this->aStructuredQuery;
351
352                         if (sizeof($aParams) == 1) return false;
353
354                         $aOrderToFallback = array('postalcode', 'street', 'city', 'county', 'state');
355
356                         foreach($aOrderToFallback as $sType)
357                         {
358                                 if (isset($aParams[$sType]))
359                                 {
360                                         unset($aParams[$sType]);
361                                         $this->setStructuredQuery(@$aParams['amenity'], @$aParams['street'], @$aParams['city'], @$aParams['county'], @$aParams['state'], @$aParams['country'], @$aParams['postalcode']);
362                                         return true;
363                                 }
364                         }
365
366                         return false;
367                 }
368
369                 function getDetails($aPlaceIDs)
370                 {
371                         if (sizeof($aPlaceIDs) == 0)  return array();
372
373                         $sLanguagePrefArraySQL = "ARRAY[".join(',',array_map("getDBQuoted",$this->aLangPrefOrder))."]";
374
375                         // Get the details for display (is this a redundant extra step?)
376                         $sPlaceIDs = join(',',$aPlaceIDs);
377
378                         $sImportanceSQL = '';
379                         if ($this->sViewboxSmallSQL) $sImportanceSQL .= " case when ST_Contains($this->sViewboxSmallSQL, ST_Collect(centroid)) THEN 1 ELSE 0.75 END * ";
380                         if ($this->sViewboxLargeSQL) $sImportanceSQL .= " case when ST_Contains($this->sViewboxLargeSQL, ST_Collect(centroid)) THEN 1 ELSE 0.75 END * ";
381
382                         $sSQL = "select osm_type,osm_id,class,type,admin_level,rank_search,rank_address,min(place_id) as place_id, min(parent_place_id) as parent_place_id, calculated_country_code as country_code,";
383                         $sSQL .= "get_address_by_language(place_id, $sLanguagePrefArraySQL) as langaddress,";
384                         $sSQL .= "get_name_by_language(name, $sLanguagePrefArraySQL) as placename,";
385                         $sSQL .= "get_name_by_language(name, ARRAY['ref']) as ref,";
386                         $sSQL .= "avg(ST_X(centroid)) as lon,avg(ST_Y(centroid)) as lat, ";
387                         $sSQL .= $sImportanceSQL."coalesce(importance,0.75-(rank_search::float/40)) as importance, ";
388                         $sSQL .= "(select max(p.importance*(p.rank_address+2)) from place_addressline s, placex p where s.place_id = min(CASE WHEN placex.rank_search < 28 THEN placex.place_id ELSE placex.parent_place_id END) and p.place_id = s.address_place_id and s.isaddress and p.importance is not null) as addressimportance, ";
389                         $sSQL .= "(extratags->'place') as extra_place ";
390                         $sSQL .= "from placex where place_id in ($sPlaceIDs) ";
391                         $sSQL .= "and (placex.rank_address between $this->iMinAddressRank and $this->iMaxAddressRank ";
392                         if (14 >= $this->iMinAddressRank && 14 <= $this->iMaxAddressRank) $sSQL .= " OR (extratags->'place') = 'city'";
393                         if ($this->aAddressRankList) $sSQL .= " OR placex.rank_address in (".join(',',$this->aAddressRankList).")";
394                         $sSQL .= ") ";
395                         if ($this->sAllowedTypesSQLList) $sSQL .= "and placex.class in $this->sAllowedTypesSQLList ";
396                         $sSQL .= "and linked_place_id is null ";
397                         $sSQL .= "group by osm_type,osm_id,class,type,admin_level,rank_search,rank_address,calculated_country_code,importance";
398                         if (!$this->bDeDupe) $sSQL .= ",place_id";
399                         $sSQL .= ",langaddress ";
400                         $sSQL .= ",placename ";
401                         $sSQL .= ",ref ";
402                         $sSQL .= ",extratags->'place' ";
403
404                         if (30 >= $this->iMinAddressRank && 30 <= $this->iMaxAddressRank)
405                         {
406                                 $sSQL .= " union ";
407                                 $sSQL .= "select 'T' as osm_type,place_id as osm_id,'place' as class,'house' as type,null as admin_level,30 as rank_search,30 as rank_address,min(place_id) as place_id, min(parent_place_id) as parent_place_id,'us' as country_code,";
408                                 $sSQL .= "get_address_by_language(place_id, $sLanguagePrefArraySQL) as langaddress,";
409                                 $sSQL .= "null as placename,";
410                                 $sSQL .= "null as ref,";
411                                 $sSQL .= "avg(ST_X(centroid)) as lon,avg(ST_Y(centroid)) as lat, ";
412                                 $sSQL .= $sImportanceSQL."-1.15 as importance, ";
413                                 $sSQL .= "(select max(p.importance*(p.rank_address+2)) from place_addressline s, placex p where s.place_id = min(location_property_tiger.parent_place_id) and p.place_id = s.address_place_id and s.isaddress and p.importance is not null) as addressimportance, ";
414                                 $sSQL .= "null as extra_place ";
415                                 $sSQL .= "from location_property_tiger where place_id in ($sPlaceIDs) ";
416                                 $sSQL .= "and 30 between $this->iMinAddressRank and $this->iMaxAddressRank ";
417                                 $sSQL .= "group by place_id";
418                                 if (!$this->bDeDupe) $sSQL .= ",place_id ";
419                                 $sSQL .= " union ";
420                                 $sSQL .= "select 'L' as osm_type,place_id as osm_id,'place' as class,'house' as type,null as admin_level,30 as rank_search,30 as rank_address,min(place_id) as place_id, min(parent_place_id) as parent_place_id,'us' as country_code,";
421                                 $sSQL .= "get_address_by_language(place_id, $sLanguagePrefArraySQL) as langaddress,";
422                                 $sSQL .= "null as placename,";
423                                 $sSQL .= "null as ref,";
424                                 $sSQL .= "avg(ST_X(centroid)) as lon,avg(ST_Y(centroid)) as lat, ";
425                                 $sSQL .= $sImportanceSQL."-1.10 as importance, ";
426                                 $sSQL .= "(select max(p.importance*(p.rank_address+2)) from place_addressline s, placex p where s.place_id = min(location_property_aux.parent_place_id) and p.place_id = s.address_place_id and s.isaddress and p.importance is not null) as addressimportance, ";
427                                 $sSQL .= "null as extra_place ";
428                                 $sSQL .= "from location_property_aux where place_id in ($sPlaceIDs) ";
429                                 $sSQL .= "and 30 between $this->iMinAddressRank and $this->iMaxAddressRank ";
430                                 $sSQL .= "group by place_id";
431                                 if (!$this->bDeDupe) $sSQL .= ",place_id";
432                                 $sSQL .= ",get_address_by_language(place_id, $sLanguagePrefArraySQL) ";
433                         }
434
435                         $sSQL .= " order by importance desc";
436                         if (CONST_Debug) { echo "<hr>"; var_dump($sSQL); }
437                         $aSearchResults = $this->oDB->getAll($sSQL);
438
439                         if (PEAR::IsError($aSearchResults))
440                         {
441                                 failInternalError("Could not get details for place.", $sSQL, $aSearchResults);
442                         }
443
444                         return $aSearchResults;
445                 }
446
447                 function getGroupedSearches($aSearches, $aPhraseTypes, $aPhrases, $aValidTokens, $aWordFrequencyScores, $bStructuredPhrases)
448                 {
449                         /*
450                            Calculate all searches using aValidTokens i.e.
451                            'Wodsworth Road, Sheffield' =>
452
453                            Phrase Wordset
454                            0      0       (wodsworth road)
455                            0      1       (wodsworth)(road)
456                            1      0       (sheffield)
457
458                            Score how good the search is so they can be ordered
459                          */
460                         foreach($aPhrases as $iPhrase => $sPhrase)
461                         {
462                                 $aNewPhraseSearches = array();
463                                 if ($bStructuredPhrases) $sPhraseType = $aPhraseTypes[$iPhrase];
464                                 else $sPhraseType = '';
465
466                                 foreach($aPhrases[$iPhrase]['wordsets'] as $iWordSet => $aWordset)
467                                 {
468                                         // Too many permutations - too expensive
469                                         if ($iWordSet > 120) break;
470
471                                         $aWordsetSearches = $aSearches;
472
473                                         // Add all words from this wordset
474                                         foreach($aWordset as $iToken => $sToken)
475                                         {
476                                                 //echo "<br><b>$sToken</b>";
477                                                 $aNewWordsetSearches = array();
478
479                                                 foreach($aWordsetSearches as $aCurrentSearch)
480                                                 {
481                                                         //echo "<i>";
482                                                         //var_dump($aCurrentSearch);
483                                                         //echo "</i>";
484
485                                                         // If the token is valid
486                                                         if (isset($aValidTokens[' '.$sToken]))
487                                                         {
488                                                                 foreach($aValidTokens[' '.$sToken] as $aSearchTerm)
489                                                                 {
490                                                                         $aSearch = $aCurrentSearch;
491                                                                         $aSearch['iSearchRank']++;
492                                                                         if (($sPhraseType == '' || $sPhraseType == 'country') && !empty($aSearchTerm['country_code']) && $aSearchTerm['country_code'] != '0')
493                                                                         {
494                                                                                 if ($aSearch['sCountryCode'] === false)
495                                                                                 {
496                                                                                         $aSearch['sCountryCode'] = strtolower($aSearchTerm['country_code']);
497                                                                                         // Country is almost always at the end of the string - increase score for finding it anywhere else (optimisation)
498                                                                                         if (($iToken+1 != sizeof($aWordset) || $iPhrase+1 != sizeof($aPhrases)))
499                                                                                         {
500                                                                                                 $aSearch['iSearchRank'] += 5;
501                                                                                         }
502                                                                                         if ($aSearch['iSearchRank'] < $this->iMaxRank) $aNewWordsetSearches[] = $aSearch;
503                                                                                 }
504                                                                         }
505                                                                         elseif (isset($aSearchTerm['lat']) && $aSearchTerm['lat'] !== '' && $aSearchTerm['lat'] !== null)
506                                                                         {
507                                                                                 if ($aSearch['fLat'] === '')
508                                                                                 {
509                                                                                         $aSearch['fLat'] = $aSearchTerm['lat'];
510                                                                                         $aSearch['fLon'] = $aSearchTerm['lon'];
511                                                                                         $aSearch['fRadius'] = $aSearchTerm['radius'];
512                                                                                         if ($aSearch['iSearchRank'] < $this->iMaxRank) $aNewWordsetSearches[] = $aSearch;
513                                                                                 }
514                                                                         }
515                                                                         elseif ($sPhraseType == 'postalcode')
516                                                                         {
517                                                                                 // We need to try the case where the postal code is the primary element (i.e. no way to tell if it is (postalcode, city) OR (city, postalcode) so try both
518                                                                                 if (isset($aSearchTerm['word_id']) && $aSearchTerm['word_id'])
519                                                                                 {
520                                                                                         // If we already have a name try putting the postcode first
521                                                                                         if (sizeof($aSearch['aName']))
522                                                                                         {
523                                                                                                 $aNewSearch = $aSearch;
524                                                                                                 $aNewSearch['aAddress'] = array_merge($aNewSearch['aAddress'], $aNewSearch['aName']);
525                                                                                                 $aNewSearch['aName'] = array();
526                                                                                                 $aNewSearch['aName'][$aSearchTerm['word_id']] = $aSearchTerm['word_id'];
527                                                                                                 if ($aSearch['iSearchRank'] < $this->iMaxRank) $aNewWordsetSearches[] = $aNewSearch;
528                                                                                         }
529
530                                                                                         if (sizeof($aSearch['aName']))
531                                                                                         {
532                                                                                                 if ((!$bStructuredPhrases || $iPhrase > 0) && $sPhraseType != 'country' && (!isset($aValidTokens[$sToken]) || strpos($sToken, ' ') !== false))
533                                                                                                 {
534                                                                                                         $aSearch['aAddress'][$aSearchTerm['word_id']] = $aSearchTerm['word_id'];
535                                                                                                 }
536                                                                                                 else
537                                                                                                 {
538                                                                                                         $aCurrentSearch['aFullNameAddress'][$aSearchTerm['word_id']] = $aSearchTerm['word_id'];
539                                                                                                         $aSearch['iSearchRank'] += 1000; // skip;
540                                                                                                 }
541                                                                                         }
542                                                                                         else
543                                                                                         {
544                                                                                                 $aSearch['aName'][$aSearchTerm['word_id']] = $aSearchTerm['word_id'];
545                                                                                                 //$aSearch['iNamePhrase'] = $iPhrase;
546                                                                                         }
547                                                                                         if ($aSearch['iSearchRank'] < $this->iMaxRank) $aNewWordsetSearches[] = $aSearch;
548                                                                                 }
549
550                                                                         }
551                                                                         elseif (($sPhraseType == '' || $sPhraseType == 'street') && $aSearchTerm['class'] == 'place' && $aSearchTerm['type'] == 'house')
552                                                                         {
553                                                                                 if ($aSearch['sHouseNumber'] === '')
554                                                                                 {
555                                                                                         $aSearch['sHouseNumber'] = $sToken;
556                                                                                         // sanity check: if the housenumber is not mainly made
557                                                                                         // up of numbers, add a penalty
558                                                                                         if (preg_match_all("/[^0-9]/", $sToken, $aMatches) > 2) $aSearch['iSearchRank']++;
559                                                                                         // also housenumbers should appear in the first or second phrase
560                                                                                         if ($iPhrase > 1) $aSearch['iSearchRank'] += 1;
561                                                                                         if ($aSearch['iSearchRank'] < $this->iMaxRank) $aNewWordsetSearches[] = $aSearch;
562                                                                                         /*
563                                                                                         // Fall back to not searching for this item (better than nothing)
564                                                                                         $aSearch = $aCurrentSearch;
565                                                                                         $aSearch['iSearchRank'] += 1;
566                                                                                         if ($aSearch['iSearchRank'] < $this->iMaxRank) $aNewWordsetSearches[] = $aSearch;
567                                                                                          */
568                                                                                 }
569                                                                         }
570                                                                         elseif ($sPhraseType == '' && $aSearchTerm['class'] !== '' && $aSearchTerm['class'] !== null)
571                                                                         {
572                                                                                 if ($aSearch['sClass'] === '')
573                                                                                 {
574                                                                                         $aSearch['sOperator'] = $aSearchTerm['operator'];
575                                                                                         $aSearch['sClass'] = $aSearchTerm['class'];
576                                                                                         $aSearch['sType'] = $aSearchTerm['type'];
577                                                                                         if (sizeof($aSearch['aName'])) $aSearch['sOperator'] = 'name';
578                                                                                         else $aSearch['sOperator'] = 'near'; // near = in for the moment
579                                                                                         if (strlen($aSearchTerm['operator']) == 0) $aSearch['iSearchRank'] += 1;
580
581                                                                                         // Do we have a shortcut id?
582                                                                                         if ($aSearch['sOperator'] == 'name')
583                                                                                         {
584                                                                                                 $sSQL = "select get_tagpair('".$aSearch['sClass']."', '".$aSearch['sType']."')";
585                                                                                                 if ($iAmenityID = $this->oDB->getOne($sSQL))
586                                                                                                 {
587                                                                                                         $aValidTokens[$aSearch['sClass'].':'.$aSearch['sType']] = array('word_id' => $iAmenityID);
588                                                                                                         $aSearch['aName'][$iAmenityID] = $iAmenityID;
589                                                                                                         $aSearch['sClass'] = '';
590                                                                                                         $aSearch['sType'] = '';
591                                                                                                 }
592                                                                                         }
593                                                                                         if ($aSearch['iSearchRank'] < $this->iMaxRank) $aNewWordsetSearches[] = $aSearch;
594                                                                                 }
595                                                                         }
596                                                                         elseif (isset($aSearchTerm['word_id']) && $aSearchTerm['word_id'])
597                                                                         {
598                                                                                 if (sizeof($aSearch['aName']))
599                                                                                 {
600                                                                                         if ((!$bStructuredPhrases || $iPhrase > 0) && $sPhraseType != 'country' && (!isset($aValidTokens[$sToken]) || strpos($sToken, ' ') !== false))
601                                                                                         {
602                                                                                                 $aSearch['aAddress'][$aSearchTerm['word_id']] = $aSearchTerm['word_id'];
603                                                                                         }
604                                                                                         else
605                                                                                         {
606                                                                                                 $aCurrentSearch['aFullNameAddress'][$aSearchTerm['word_id']] = $aSearchTerm['word_id'];
607                                                                                                 $aSearch['iSearchRank'] += 1000; // skip;
608                                                                                         }
609                                                                                 }
610                                                                                 else
611                                                                                 {
612                                                                                         $aSearch['aName'][$aSearchTerm['word_id']] = $aSearchTerm['word_id'];
613                                                                                         //$aSearch['iNamePhrase'] = $iPhrase;
614                                                                                 }
615                                                                                 if ($aSearch['iSearchRank'] < $this->iMaxRank) $aNewWordsetSearches[] = $aSearch;
616                                                                         }
617                                                                 }
618                                                         }
619                                                         // Look for partial matches.
620                                                         // Note that there is no point in adding country terms here
621                                                         // because country are omitted in the address.
622                                                         if (isset($aValidTokens[$sToken]) && $sPhraseType != 'country')
623                                                         {
624                                                                 // Allow searching for a word - but at extra cost
625                                                                 foreach($aValidTokens[$sToken] as $aSearchTerm)
626                                                                 {
627                                                                         if (isset($aSearchTerm['word_id']) && $aSearchTerm['word_id'])
628                                                                         {
629                                                                                 if ((!$bStructuredPhrases || $iPhrase > 0) && sizeof($aCurrentSearch['aName']) && strpos($sToken, ' ') === false)
630                                                                                 {
631                                                                                         $aSearch = $aCurrentSearch;
632                                                                                         $aSearch['iSearchRank'] += 1;
633                                                                                         if ($aWordFrequencyScores[$aSearchTerm['word_id']] < CONST_Max_Word_Frequency)
634                                                                                         {
635                                                                                                 $aSearch['aAddress'][$aSearchTerm['word_id']] = $aSearchTerm['word_id'];
636                                                                                                 if ($aSearch['iSearchRank'] < $this->iMaxRank) $aNewWordsetSearches[] = $aSearch;
637                                                                                         }
638                                                                                         elseif (isset($aValidTokens[' '.$sToken])) // revert to the token version?
639                                                                                         {
640                                                                                                 $aSearch['aAddressNonSearch'][$aSearchTerm['word_id']] = $aSearchTerm['word_id'];
641                                                                                                 $aSearch['iSearchRank'] += 1;
642                                                                                                 if ($aSearch['iSearchRank'] < $this->iMaxRank) $aNewWordsetSearches[] = $aSearch;
643                                                                                                 foreach($aValidTokens[' '.$sToken] as $aSearchTermToken)
644                                                                                                 {
645                                                                                                         if (empty($aSearchTermToken['country_code'])
646                                                                                                                         && empty($aSearchTermToken['lat'])
647                                                                                                                         && empty($aSearchTermToken['class']))
648                                                                                                         {
649                                                                                                                 $aSearch = $aCurrentSearch;
650                                                                                                                 $aSearch['iSearchRank'] += 1;
651                                                                                                                 $aSearch['aAddress'][$aSearchTermToken['word_id']] = $aSearchTermToken['word_id'];
652                                                                                                                 if ($aSearch['iSearchRank'] < $this->iMaxRank) $aNewWordsetSearches[] = $aSearch;
653                                                                                                         }
654                                                                                                 }
655                                                                                         }
656                                                                                         else
657                                                                                         {
658                                                                                                 $aSearch['aAddressNonSearch'][$aSearchTerm['word_id']] = $aSearchTerm['word_id'];
659                                                                                                 if (preg_match('#^[0-9]+$#', $sToken)) $aSearch['iSearchRank'] += 2;
660                                                                                                 if ($aSearch['iSearchRank'] < $this->iMaxRank) $aNewWordsetSearches[] = $aSearch;
661                                                                                         }
662                                                                                 }
663
664                                                                                 if (!sizeof($aCurrentSearch['aName']) || $aCurrentSearch['iNamePhrase'] == $iPhrase)
665                                                                                 {
666                                                                                         $aSearch = $aCurrentSearch;
667                                                                                         $aSearch['iSearchRank'] += 1;
668                                                                                         if (!sizeof($aCurrentSearch['aName'])) $aSearch['iSearchRank'] += 1;
669                                                                                         if (preg_match('#^[0-9]+$#', $sToken)) $aSearch['iSearchRank'] += 2;
670                                                                                         if ($aWordFrequencyScores[$aSearchTerm['word_id']] < CONST_Max_Word_Frequency)
671                                                                                                 $aSearch['aName'][$aSearchTerm['word_id']] = $aSearchTerm['word_id'];
672                                                                                         else
673                                                                                                 $aSearch['aNameNonSearch'][$aSearchTerm['word_id']] = $aSearchTerm['word_id'];
674                                                                                         $aSearch['iNamePhrase'] = $iPhrase;
675                                                                                         if ($aSearch['iSearchRank'] < $this->iMaxRank) $aNewWordsetSearches[] = $aSearch;
676                                                                                 }
677                                                                         }
678                                                                 }
679                                                         }
680                                                         else
681                                                         {
682                                                                 // Allow skipping a word - but at EXTREAM cost
683                                                                 //$aSearch = $aCurrentSearch;
684                                                                 //$aSearch['iSearchRank']+=100;
685                                                                 //$aNewWordsetSearches[] = $aSearch;
686                                                         }
687                                                 }
688                                                 // Sort and cut
689                                                 usort($aNewWordsetSearches, 'bySearchRank');
690                                                 $aWordsetSearches = array_slice($aNewWordsetSearches, 0, 50);
691                                         }
692                                         //var_Dump('<hr>',sizeof($aWordsetSearches)); exit;
693
694                                         $aNewPhraseSearches = array_merge($aNewPhraseSearches, $aNewWordsetSearches);
695                                         usort($aNewPhraseSearches, 'bySearchRank');
696
697                                         $aSearchHash = array();
698                                         foreach($aNewPhraseSearches as $iSearch => $aSearch)
699                                         {
700                                                 $sHash = serialize($aSearch);
701                                                 if (isset($aSearchHash[$sHash])) unset($aNewPhraseSearches[$iSearch]);
702                                                 else $aSearchHash[$sHash] = 1;
703                                         }
704
705                                         $aNewPhraseSearches = array_slice($aNewPhraseSearches, 0, 50);
706                                 }
707
708                                 // Re-group the searches by their score, junk anything over 20 as just not worth trying
709                                 $aGroupedSearches = array();
710                                 foreach($aNewPhraseSearches as $aSearch)
711                                 {
712                                         if ($aSearch['iSearchRank'] < $this->iMaxRank)
713                                         {
714                                                 if (!isset($aGroupedSearches[$aSearch['iSearchRank']])) $aGroupedSearches[$aSearch['iSearchRank']] = array();
715                                                 $aGroupedSearches[$aSearch['iSearchRank']][] = $aSearch;
716                                         }
717                                 }
718                                 ksort($aGroupedSearches);
719
720                                 $iSearchCount = 0;
721                                 $aSearches = array();
722                                 foreach($aGroupedSearches as $iScore => $aNewSearches)
723                                 {
724                                         $iSearchCount += sizeof($aNewSearches);
725                                         $aSearches = array_merge($aSearches, $aNewSearches);
726                                         if ($iSearchCount > 50) break;
727                                 }
728
729                                 //if (CONST_Debug) _debugDumpGroupedSearches($aGroupedSearches, $aValidTokens);
730
731                         }
732                         return $aGroupedSearches;
733
734                 }
735
736                 /* Perform the actual query lookup.
737
738                         Returns an ordered list of results, each with the following fields:
739                           osm_type: type of corresponding OSM object
740                                                         N - node
741                                                         W - way
742                                                         R - relation
743                                                         P - postcode (internally computed)
744                           osm_id: id of corresponding OSM object
745                           class: general object class (corresponds to tag key of primary OSM tag)
746                           type: subclass of object (corresponds to tag value of primary OSM tag)
747                           admin_level: see http://wiki.openstreetmap.org/wiki/Admin_level
748                           rank_search: rank in search hierarchy
749                                                         (see also http://wiki.openstreetmap.org/wiki/Nominatim/Development_overview#Country_to_street_level)
750                           rank_address: rank in address hierarchy (determines orer in address)
751                           place_id: internal key (may differ between different instances)
752                           country_code: ISO country code
753                           langaddress: localized full address
754                           placename: localized name of object
755                           ref: content of ref tag (if available)
756                           lon: longitude
757                           lat: latitude
758                           importance: importance of place based on Wikipedia link count
759                           addressimportance: cumulated importance of address elements
760                           extra_place: type of place (for admin boundaries, if there is a place tag)
761                           aBoundingBox: bounding Box
762                           label: short description of the object class/type (English only) 
763                           name: full name (currently the same as langaddress)
764                           foundorder: secondary ordering for places with same importance
765                 */
766                 function lookup()
767                 {
768                         if (!$this->sQuery && !$this->aStructuredQuery) return false;
769
770                         $sLanguagePrefArraySQL = "ARRAY[".join(',',array_map("getDBQuoted",$this->aLangPrefOrder))."]";
771
772                         $sCountryCodesSQL = false;
773                         if ($this->aCountryCodes && sizeof($this->aCountryCodes))
774                         {
775                                 $sCountryCodesSQL = join(',', array_map('addQuotes', $this->aCountryCodes));
776                         }
777
778                         $sQuery = $this->sQuery;
779
780                         // Conflicts between US state abreviations and various words for 'the' in different languages
781                         if (isset($this->aLangPrefOrder['name:en']))
782                         {
783                                 $sQuery = preg_replace('/(^|,)\s*il\s*(,|$)/','\1illinois\2', $sQuery);
784                                 $sQuery = preg_replace('/(^|,)\s*al\s*(,|$)/','\1alabama\2', $sQuery);
785                                 $sQuery = preg_replace('/(^|,)\s*la\s*(,|$)/','\1louisiana\2', $sQuery);
786                         }
787
788                         // View Box SQL
789                         $sViewboxCentreSQL = false;
790                         $bBoundingBoxSearch = false;
791                         if ($this->aViewBox)
792                         {
793                                 $fHeight = $this->aViewBox[0]-$this->aViewBox[2];
794                                 $fWidth = $this->aViewBox[1]-$this->aViewBox[3];
795                                 $aBigViewBox[0] = $this->aViewBox[0] + $fHeight;
796                                 $aBigViewBox[2] = $this->aViewBox[2] - $fHeight;
797                                 $aBigViewBox[1] = $this->aViewBox[1] + $fWidth;
798                                 $aBigViewBox[3] = $this->aViewBox[3] - $fWidth;
799
800                                 $this->sViewboxSmallSQL = "ST_SetSRID(ST_MakeBox2D(ST_Point(".(float)$this->aViewBox[0].",".(float)$this->aViewBox[1]."),ST_Point(".(float)$this->aViewBox[2].",".(float)$this->aViewBox[3].")),4326)";
801                                 $this->sViewboxLargeSQL = "ST_SetSRID(ST_MakeBox2D(ST_Point(".(float)$aBigViewBox[0].",".(float)$aBigViewBox[1]."),ST_Point(".(float)$aBigViewBox[2].",".(float)$aBigViewBox[3].")),4326)";
802                                 $bBoundingBoxSearch = $this->bBoundedSearch;
803                         }
804
805                         // Route SQL
806                         if ($this->aRoutePoints)
807                         {
808                                 $sViewboxCentreSQL = "ST_SetSRID('LINESTRING(";
809                                 $bFirst = true;
810                                 foreach($this->aRoutePoints as $aPoint)
811                                 {
812                                         if (!$bFirst) $sViewboxCentreSQL .= ",";
813                                         $sViewboxCentreSQL .= $aPoint[0].' '.$aPoint[1];
814                                         $bFirst = false;
815                                 }
816                                 $sViewboxCentreSQL .= ")'::geometry,4326)";
817
818                                 $sSQL = "select st_buffer(".$sViewboxCentreSQL.",".(float)($_GET['routewidth']/69).")";
819                                 $this->sViewboxSmallSQL = $this->oDB->getOne($sSQL);
820                                 if (PEAR::isError($this->sViewboxSmallSQL))
821                                 {
822                                         failInternalError("Could not get small viewbox.", $sSQL, $this->sViewboxSmallSQL);
823                                 }
824                                 $this->sViewboxSmallSQL = "'".$this->sViewboxSmallSQL."'::geometry";
825
826                                 $sSQL = "select st_buffer(".$sViewboxCentreSQL.",".(float)($_GET['routewidth']/30).")";
827                                 $this->sViewboxLargeSQL = $this->oDB->getOne($sSQL);
828                                 if (PEAR::isError($this->sViewboxLargeSQL))
829                                 {
830                                         failInternalError("Could not get large viewbox.", $sSQL, $this->sViewboxLargeSQL);
831                                 }
832                                 $this->sViewboxLargeSQL = "'".$this->sViewboxLargeSQL."'::geometry";
833                                 $bBoundingBoxSearch = $this->bBoundedSearch;
834                         }
835
836                         // Do we have anything that looks like a lat/lon pair?
837                         if ( $aLooksLike = looksLikeLatLonPair($sQuery) ){
838                                 $this->setNearPoint(array($aLooksLike['lat'], $aLooksLike['lon']));
839                                 $sQuery = $aLooksLike['query'];                 
840                         }
841
842                         $aSearchResults = array();
843                         if ($sQuery || $this->aStructuredQuery)
844                         {
845                                 // Start with a blank search
846                                 $aSearches = array(
847                                         array('iSearchRank' => 0, 'iNamePhrase' => -1, 'sCountryCode' => false, 'aName'=>array(), 'aAddress'=>array(), 'aFullNameAddress'=>array(),
848                                               'aNameNonSearch'=>array(), 'aAddressNonSearch'=>array(),
849                                               'sOperator'=>'', 'aFeatureName' => array(), 'sClass'=>'', 'sType'=>'', 'sHouseNumber'=>'', 'fLat'=>'', 'fLon'=>'', 'fRadius'=>'')
850                                 );
851
852                                 // Do we have a radius search?
853                                 $sNearPointSQL = false;
854                                 if ($this->aNearPoint)
855                                 {
856                                         $sNearPointSQL = "ST_SetSRID(ST_Point(".(float)$this->aNearPoint[1].",".(float)$this->aNearPoint[0]."),4326)";
857                                         $aSearches[0]['fLat'] = (float)$this->aNearPoint[0];
858                                         $aSearches[0]['fLon'] = (float)$this->aNearPoint[1];
859                                         $aSearches[0]['fRadius'] = (float)$this->aNearPoint[2];
860                                 }
861
862                                 // Any 'special' terms in the search?
863                                 $bSpecialTerms = false;
864                                 preg_match_all('/\\[(.*)=(.*)\\]/', $sQuery, $aSpecialTermsRaw, PREG_SET_ORDER);
865                                 $aSpecialTerms = array();
866                                 foreach($aSpecialTermsRaw as $aSpecialTerm)
867                                 {
868                                         $sQuery = str_replace($aSpecialTerm[0], ' ', $sQuery);
869                                         $aSpecialTerms[strtolower($aSpecialTerm[1])] = $aSpecialTerm[2];
870                                 }
871
872                                 preg_match_all('/\\[([\\w ]*)\\]/u', $sQuery, $aSpecialTermsRaw, PREG_SET_ORDER);
873                                 $aSpecialTerms = array();
874                                 if (isset($aStructuredQuery['amenity']) && $aStructuredQuery['amenity'])
875                                 {
876                                         $aSpecialTermsRaw[] = array('['.$aStructuredQuery['amenity'].']', $aStructuredQuery['amenity']);
877                                         unset($aStructuredQuery['amenity']);
878                                 }
879                                 foreach($aSpecialTermsRaw as $aSpecialTerm)
880                                 {
881                                         $sQuery = str_replace($aSpecialTerm[0], ' ', $sQuery);
882                                         $sToken = $this->oDB->getOne("select make_standard_name('".$aSpecialTerm[1]."') as string");
883                                         $sSQL = 'select * from (select word_id,word_token, word, class, type, country_code, operator';
884                                         $sSQL .= ' from word where word_token in (\' '.$sToken.'\')) as x where (class is not null and class not in (\'place\')) or country_code is not null';
885                                         if (CONST_Debug) var_Dump($sSQL);
886                                         $aSearchWords = $this->oDB->getAll($sSQL);
887                                         $aNewSearches = array();
888                                         foreach($aSearches as $aSearch)
889                                         {
890                                                 foreach($aSearchWords as $aSearchTerm)
891                                                 {
892                                                         $aNewSearch = $aSearch;
893                                                         if ($aSearchTerm['country_code'])
894                                                         {
895                                                                 $aNewSearch['sCountryCode'] = strtolower($aSearchTerm['country_code']);
896                                                                 $aNewSearches[] = $aNewSearch;
897                                                                 $bSpecialTerms = true;
898                                                         }
899                                                         if ($aSearchTerm['class'])
900                                                         {
901                                                                 $aNewSearch['sClass'] = $aSearchTerm['class'];
902                                                                 $aNewSearch['sType'] = $aSearchTerm['type'];
903                                                                 $aNewSearches[] = $aNewSearch;
904                                                                 $bSpecialTerms = true;
905                                                         }
906                                                 }
907                                         }
908                                         $aSearches = $aNewSearches;
909                                 }
910
911                                 // Split query into phrases
912                                 // Commas are used to reduce the search space by indicating where phrases split
913                                 if ($this->aStructuredQuery)
914                                 {
915                                         $aPhrases = $this->aStructuredQuery;
916                                         $bStructuredPhrases = true;
917                                 }
918                                 else
919                                 {
920                                         $aPhrases = explode(',',$sQuery);
921                                         $bStructuredPhrases = false;
922                                 }
923
924                                 // Convert each phrase to standard form
925                                 // Create a list of standard words
926                                 // Get all 'sets' of words
927                                 // Generate a complete list of all
928                                 $aTokens = array();
929                                 foreach($aPhrases as $iPhrase => $sPhrase)
930                                 {
931                                         $aPhrase = $this->oDB->getRow("select make_standard_name('".pg_escape_string($sPhrase)."') as string");
932                                         if (PEAR::isError($aPhrase))
933                                         {
934                                                 userError("Illegal query string (not an UTF-8 string): ".$sPhrase);
935                                                 if (CONST_Debug) var_dump($aPhrase);
936                                                 exit;
937                                         }
938                                         if (trim($aPhrase['string']))
939                                         {
940                                                 $aPhrases[$iPhrase] = $aPhrase;
941                                                 $aPhrases[$iPhrase]['words'] = explode(' ',$aPhrases[$iPhrase]['string']);
942                                                 $aPhrases[$iPhrase]['wordsets'] = getWordSets($aPhrases[$iPhrase]['words'], 0);
943                                                 $aTokens = array_merge($aTokens, getTokensFromSets($aPhrases[$iPhrase]['wordsets']));
944                                         }
945                                         else
946                                         {
947                                                 unset($aPhrases[$iPhrase]);
948                                         }
949                                 }
950
951                                 // Reindex phrases - we make assumptions later on that they are numerically keyed in order
952                                 $aPhraseTypes = array_keys($aPhrases);
953                                 $aPhrases = array_values($aPhrases);
954
955                                 if (sizeof($aTokens))
956                                 {
957                                         // Check which tokens we have, get the ID numbers
958                                         $sSQL = 'select word_id,word_token, word, class, type, country_code, operator, search_name_count';
959                                         $sSQL .= ' from word where word_token in ('.join(',',array_map("getDBQuoted",$aTokens)).')';
960
961                                         if (CONST_Debug) var_Dump($sSQL);
962
963                                         $aValidTokens = array();
964                                         if (sizeof($aTokens)) $aDatabaseWords = $this->oDB->getAll($sSQL);
965                                         else $aDatabaseWords = array();
966                                         if (PEAR::IsError($aDatabaseWords))
967                                         {
968                                                 failInternalError("Could not get word tokens.", $sSQL, $aDatabaseWords);
969                                         }
970                                         $aPossibleMainWordIDs = array();
971                                         $aWordFrequencyScores = array();
972                                         foreach($aDatabaseWords as $aToken)
973                                         {
974                                                 // Very special case - require 2 letter country param to match the country code found
975                                                 if ($bStructuredPhrases && $aToken['country_code'] && !empty($aStructuredQuery['country'])
976                                                                 && strlen($aStructuredQuery['country']) == 2 && strtolower($aStructuredQuery['country']) != $aToken['country_code'])
977                                                 {
978                                                         continue;
979                                                 }
980
981                                                 if (isset($aValidTokens[$aToken['word_token']]))
982                                                 {
983                                                         $aValidTokens[$aToken['word_token']][] = $aToken;
984                                                 }
985                                                 else
986                                                 {
987                                                         $aValidTokens[$aToken['word_token']] = array($aToken);
988                                                 }
989                                                 if (!$aToken['class'] && !$aToken['country_code']) $aPossibleMainWordIDs[$aToken['word_id']] = 1;
990                                                 $aWordFrequencyScores[$aToken['word_id']] = $aToken['search_name_count'] + 1;
991                                         }
992                                         if (CONST_Debug) var_Dump($aPhrases, $aValidTokens);
993
994                                         // Try and calculate GB postcodes we might be missing
995                                         foreach($aTokens as $sToken)
996                                         {
997                                                 // Source of gb postcodes is now definitive - always use
998                                                 if (preg_match('/^([A-Z][A-Z]?[0-9][0-9A-Z]? ?[0-9])([A-Z][A-Z])$/', strtoupper(trim($sToken)), $aData))
999                                                 {
1000                                                         if (substr($aData[1],-2,1) != ' ')
1001                                                         {
1002                                                                 $aData[0] = substr($aData[0],0,strlen($aData[1])-1).' '.substr($aData[0],strlen($aData[1])-1);
1003                                                                 $aData[1] = substr($aData[1],0,-1).' '.substr($aData[1],-1,1);
1004                                                         }
1005                                                         $aGBPostcodeLocation = gbPostcodeCalculate($aData[0], $aData[1], $aData[2], $this->oDB);
1006                                                         if ($aGBPostcodeLocation)
1007                                                         {
1008                                                                 $aValidTokens[$sToken] = $aGBPostcodeLocation;
1009                                                         }
1010                                                 }
1011                                                 // US ZIP+4 codes - if there is no token,
1012                                                 //      merge in the 5-digit ZIP code
1013                                                 else if (!isset($aValidTokens[$sToken]) && preg_match('/^([0-9]{5}) [0-9]{4}$/', $sToken, $aData))
1014                                                 {
1015                                                         if (isset($aValidTokens[$aData[1]]))
1016                                                         {
1017                                                                 foreach($aValidTokens[$aData[1]] as $aToken)
1018                                                                 {
1019                                                                         if (!$aToken['class'])
1020                                                                         {
1021                                                                                 if (isset($aValidTokens[$sToken]))
1022                                                                                 {
1023                                                                                         $aValidTokens[$sToken][] = $aToken;
1024                                                                                 }
1025                                                                                 else
1026                                                                                 {
1027                                                                                         $aValidTokens[$sToken] = array($aToken);
1028                                                                                 }
1029                                                                         }
1030                                                                 }
1031                                                         }
1032                                                 }
1033                                         }
1034
1035                                         foreach($aTokens as $sToken)
1036                                         {
1037                                                 // Unknown single word token with a number - assume it is a house number
1038                                                 if (!isset($aValidTokens[' '.$sToken]) && strpos($sToken,' ') === false && preg_match('/[0-9]/', $sToken))
1039                                                 {
1040                                                         $aValidTokens[' '.$sToken] = array(array('class'=>'place','type'=>'house'));
1041                                                 }
1042                                         }
1043
1044                                         // Any words that have failed completely?
1045                                         // TODO: suggestions
1046
1047                                         // Start the search process
1048                                         $aResultPlaceIDs = array();
1049
1050                                         $aGroupedSearches = $this->getGroupedSearches($aSearches, $aPhraseTypes, $aPhrases, $aValidTokens, $aWordFrequencyScores, $bStructuredPhrases);
1051
1052                                         if ($this->bReverseInPlan)
1053                                         {
1054                                                 // Reverse phrase array and also reverse the order of the wordsets in
1055                                                 // the first and final phrase. Don't bother about phrases in the middle
1056                                                 // because order in the address doesn't matter.
1057                                                 $aPhrases = array_reverse($aPhrases);
1058                                                 $aPhrases[0]['wordsets'] = getInverseWordSets($aPhrases[0]['words'], 0);
1059                                                 if (sizeof($aPhrases) > 1)
1060                                                 {
1061                                                         $aFinalPhrase = end($aPhrases);
1062                                                         $aPhrases[sizeof($aPhrases)-1]['wordsets'] = getInverseWordSets($aFinalPhrase['words'], 0);
1063                                                 }
1064                                                 $aReverseGroupedSearches = $this->getGroupedSearches($aSearches, null, $aPhrases, $aValidTokens, $aWordFrequencyScores, false);
1065
1066                                                 foreach($aGroupedSearches as $aSearches)
1067                                                 {
1068                                                         foreach($aSearches as $aSearch)
1069                                                         {
1070                                                                 if ($aSearch['iSearchRank'] < $this->iMaxRank)
1071                                                                 {
1072                                                                         if (!isset($aReverseGroupedSearches[$aSearch['iSearchRank']])) $aReverseGroupedSearches[$aSearch['iSearchRank']] = array();
1073                                                                         $aReverseGroupedSearches[$aSearch['iSearchRank']][] = $aSearch;
1074                                                                 }
1075
1076                                                         }
1077                                                 }
1078
1079                                                 $aGroupedSearches = $aReverseGroupedSearches;
1080                                                 ksort($aGroupedSearches);
1081                                         }
1082                                 }
1083                                 else
1084                                 {
1085                                         // Re-group the searches by their score, junk anything over 20 as just not worth trying
1086                                         $aGroupedSearches = array();
1087                                         foreach($aSearches as $aSearch)
1088                                         {
1089                                                 if ($aSearch['iSearchRank'] < $this->iMaxRank)
1090                                                 {
1091                                                         if (!isset($aGroupedSearches[$aSearch['iSearchRank']])) $aGroupedSearches[$aSearch['iSearchRank']] = array();
1092                                                         $aGroupedSearches[$aSearch['iSearchRank']][] = $aSearch;
1093                                                 }
1094                                         }
1095                                         ksort($aGroupedSearches);
1096                                 }
1097
1098                                 if (CONST_Debug) var_Dump($aGroupedSearches);
1099
1100                                 if (CONST_Search_TryDroppedAddressTerms && sizeof($aStructuredQuery) > 0)
1101                                 {
1102                                         $aCopyGroupedSearches = $aGroupedSearches;
1103                                         foreach($aCopyGroupedSearches as $iGroup => $aSearches)
1104                                         {
1105                                                 foreach($aSearches as $iSearch => $aSearch)
1106                                                 {
1107                                                         $aReductionsList = array($aSearch['aAddress']);
1108                                                         $iSearchRank = $aSearch['iSearchRank'];
1109                                                         while(sizeof($aReductionsList) > 0)
1110                                                         {
1111                                                                 $iSearchRank += 5;
1112                                                                 if ($iSearchRank > iMaxRank) break 3;
1113                                                                 $aNewReductionsList = array();
1114                                                                 foreach($aReductionsList as $aReductionsWordList)
1115                                                                 {
1116                                                                         for ($iReductionWord = 0; $iReductionWord < sizeof($aReductionsWordList); $iReductionWord++)
1117                                                                         {
1118                                                                                 $aReductionsWordListResult = array_merge(array_slice($aReductionsWordList, 0, $iReductionWord), array_slice($aReductionsWordList, $iReductionWord+1));
1119                                                                                 $aReverseSearch = $aSearch;
1120                                                                                 $aSearch['aAddress'] = $aReductionsWordListResult;
1121                                                                                 $aSearch['iSearchRank'] = $iSearchRank;
1122                                                                                 $aGroupedSearches[$iSearchRank][] = $aReverseSearch;
1123                                                                                 if (sizeof($aReductionsWordListResult) > 0)
1124                                                                                 {
1125                                                                                         $aNewReductionsList[] = $aReductionsWordListResult;
1126                                                                                 }
1127                                                                         }
1128                                                                 }
1129                                                                 $aReductionsList = $aNewReductionsList;
1130                                                         }
1131                                                 }
1132                                         }
1133                                         ksort($aGroupedSearches);
1134                                 }
1135
1136                                 // Filter out duplicate searches
1137                                 $aSearchHash = array();
1138                                 foreach($aGroupedSearches as $iGroup => $aSearches)
1139                                 {
1140                                         foreach($aSearches as $iSearch => $aSearch)
1141                                         {
1142                                                 $sHash = serialize($aSearch);
1143                                                 if (isset($aSearchHash[$sHash]))
1144                                                 {
1145                                                         unset($aGroupedSearches[$iGroup][$iSearch]);
1146                                                         if (sizeof($aGroupedSearches[$iGroup]) == 0) unset($aGroupedSearches[$iGroup]);
1147                                                 }
1148                                                 else
1149                                                 {
1150                                                         $aSearchHash[$sHash] = 1;
1151                                                 }
1152                                         }
1153                                 }
1154
1155                                 if (CONST_Debug) _debugDumpGroupedSearches($aGroupedSearches, $aValidTokens);
1156
1157                                 $iGroupLoop = 0;
1158                                 $iQueryLoop = 0;
1159                                 foreach($aGroupedSearches as $iGroupedRank => $aSearches)
1160                                 {
1161                                         $iGroupLoop++;
1162                                         foreach($aSearches as $aSearch)
1163                                         {
1164                                                 $iQueryLoop++;
1165
1166                                                 if (CONST_Debug) { echo "<hr><b>Search Loop, group $iGroupLoop, loop $iQueryLoop</b>"; }
1167                                                 if (CONST_Debug) _debugDumpGroupedSearches(array($iGroupedRank => array($aSearch)), $aValidTokens);
1168
1169                                                 // No location term?
1170                                                 if (!sizeof($aSearch['aName']) && !sizeof($aSearch['aAddress']) && !$aSearch['fLon'])
1171                                                 {
1172                                                         if ($aSearch['sCountryCode'] && !$aSearch['sClass'] && !$aSearch['sHouseNumber'])
1173                                                         {
1174                                                                 // Just looking for a country by code - look it up
1175                                                                 if (4 >= $this->iMinAddressRank && 4 <= $this->iMaxAddressRank)
1176                                                                 {
1177                                                                         $sSQL = "select place_id from placex where calculated_country_code='".$aSearch['sCountryCode']."' and rank_search = 4";
1178                                                                         if ($sCountryCodesSQL) $sSQL .= " and calculated_country_code in ($sCountryCodesSQL)";
1179                                     if ($bBoundingBoxSearch)
1180                                         $sSQL .= " and _st_intersects($this->sViewboxSmallSQL, geometry)";
1181                                                                         $sSQL .= " order by st_area(geometry) desc limit 1";
1182                                                                         if (CONST_Debug) var_dump($sSQL);
1183                                                                         $aPlaceIDs = $this->oDB->getCol($sSQL);
1184                                                                 }
1185                                                                 else
1186                                                                 {
1187                                                                         $aPlaceIDs = array();
1188                                                                 }
1189                                                         }
1190                                                         else
1191                                                         {
1192                                                                 if (!$bBoundingBoxSearch && !$aSearch['fLon']) continue;
1193                                                                 if (!$aSearch['sClass']) continue;
1194                                                                 $sSQL = "select count(*) from pg_tables where tablename = 'place_classtype_".$aSearch['sClass']."_".$aSearch['sType']."'";
1195                                                                 if ($this->oDB->getOne($sSQL))
1196                                                                 {
1197                                                                         $sSQL = "select place_id from place_classtype_".$aSearch['sClass']."_".$aSearch['sType']." ct";
1198                                                                         if ($sCountryCodesSQL) $sSQL .= " join placex using (place_id)";
1199                                                                         $sSQL .= " where st_contains($this->sViewboxSmallSQL, ct.centroid)";
1200                                                                         if ($sCountryCodesSQL) $sSQL .= " and calculated_country_code in ($sCountryCodesSQL)";
1201                                                                         if (sizeof($this->aExcludePlaceIDs))
1202                                                                         {
1203                                                                                 $sSQL .= " and place_id not in (".join(',',$this->aExcludePlaceIDs).")";
1204                                                                         }
1205                                                                         if ($sViewboxCentreSQL) $sSQL .= " order by st_distance($sViewboxCentreSQL, ct.centroid) asc";
1206                                                                         $sSQL .= " limit $this->iLimit";
1207                                                                         if (CONST_Debug) var_dump($sSQL);
1208                                                                         $aPlaceIDs = $this->oDB->getCol($sSQL);
1209
1210                                                                         // If excluded place IDs are given, it is fair to assume that
1211                                                                         // there have been results in the small box, so no further
1212                                                                         // expansion in that case.
1213                                                                         // Also don't expand if bounded results were requested.
1214                                                                         if (!sizeof($aPlaceIDs) && !sizeof($this->aExcludePlaceIDs) && !$this->bBoundedSearch)
1215                                                                         {
1216                                                                                 $sSQL = "select place_id from place_classtype_".$aSearch['sClass']."_".$aSearch['sType']." ct";
1217                                                                                 if ($sCountryCodesSQL) $sSQL .= " join placex using (place_id)";
1218                                                                                 $sSQL .= " where st_contains($this->sViewboxLargeSQL, ct.centroid)";
1219                                                                                 if ($sCountryCodesSQL) $sSQL .= " and calculated_country_code in ($sCountryCodesSQL)";
1220                                                                                 if ($sViewboxCentreSQL) $sSQL .= " order by st_distance($sViewboxCentreSQL, ct.centroid) asc";
1221                                                                                 $sSQL .= " limit $this->iLimit";
1222                                                                                 if (CONST_Debug) var_dump($sSQL);
1223                                                                                 $aPlaceIDs = $this->oDB->getCol($sSQL);
1224                                                                         }
1225                                                                 }
1226                                                                 else
1227                                                                 {
1228                                                                         $sSQL = "select place_id from placex where class='".$aSearch['sClass']."' and type='".$aSearch['sType']."'";
1229                                                                         $sSQL .= " and st_contains($this->sViewboxSmallSQL, geometry) and linked_place_id is null";
1230                                                                         if ($sCountryCodesSQL) $sSQL .= " and calculated_country_code in ($sCountryCodesSQL)";
1231                                                                         if ($sViewboxCentreSQL) $sSQL .= " order by st_distance($sViewboxCentreSQL, centroid) asc";
1232                                                                         $sSQL .= " limit $this->iLimit";
1233                                                                         if (CONST_Debug) var_dump($sSQL);
1234                                                                         $aPlaceIDs = $this->oDB->getCol($sSQL);
1235                                                                 }
1236                                                         }
1237                                                 }
1238                                                 else
1239                                                 {
1240                                                         $aPlaceIDs = array();
1241
1242                                                         // First we need a position, either aName or fLat or both
1243                                                         $aTerms = array();
1244                                                         $aOrder = array();
1245
1246                                                         if ($aSearch['sHouseNumber'] && sizeof($aSearch['aAddress']))
1247                                                         {
1248                                                                 $sHouseNumberRegex = '\\\\m'.$aSearch['sHouseNumber'].'\\\\M';
1249                                                                 $aOrder[] = "exists(select place_id from placex where parent_place_id = search_name.place_id and transliteration(housenumber) ~* E'".$sHouseNumberRegex."' limit 1) desc";
1250                                                         }
1251
1252                                                         // TODO: filter out the pointless search terms (2 letter name tokens and less)
1253                                                         // they might be right - but they are just too darned expensive to run
1254                                                         if (sizeof($aSearch['aName'])) $aTerms[] = "name_vector @> ARRAY[".join($aSearch['aName'],",")."]";
1255                                                         if (sizeof($aSearch['aNameNonSearch'])) $aTerms[] = "array_cat(name_vector,ARRAY[]::integer[]) @> ARRAY[".join($aSearch['aNameNonSearch'],",")."]";
1256                                                         if (sizeof($aSearch['aAddress']) && $aSearch['aName'] != $aSearch['aAddress'])
1257                                                         {
1258                                                                 // For infrequent name terms disable index usage for address
1259                                                                 if (CONST_Search_NameOnlySearchFrequencyThreshold &&
1260                                                                                 sizeof($aSearch['aName']) == 1 &&
1261                                                                                 $aWordFrequencyScores[$aSearch['aName'][reset($aSearch['aName'])]] < CONST_Search_NameOnlySearchFrequencyThreshold)
1262                                                                 {
1263                                                                         $aTerms[] = "array_cat(nameaddress_vector,ARRAY[]::integer[]) @> ARRAY[".join(array_merge($aSearch['aAddress'],$aSearch['aAddressNonSearch']),",")."]";
1264                                                                 }
1265                                                                 else
1266                                                                 {
1267                                                                         $aTerms[] = "nameaddress_vector @> ARRAY[".join($aSearch['aAddress'],",")."]";
1268                                                                         if (sizeof($aSearch['aAddressNonSearch'])) $aTerms[] = "array_cat(nameaddress_vector,ARRAY[]::integer[]) @> ARRAY[".join($aSearch['aAddressNonSearch'],",")."]";
1269                                                                 }
1270                                                         }
1271                                                         if ($aSearch['sCountryCode']) $aTerms[] = "country_code = '".pg_escape_string($aSearch['sCountryCode'])."'";
1272                                                         if ($aSearch['sHouseNumber'])
1273                                                         {
1274                                                                 $aTerms[] = "address_rank between 16 and 27";
1275                                                         }
1276                                                         else
1277                                                         {
1278                                                                 if ($this->iMinAddressRank > 0)
1279                                                                 {
1280                                                                         $aTerms[] = "address_rank >= ".$this->iMinAddressRank;
1281                                                                 }
1282                                                                 if ($this->iMaxAddressRank < 30)
1283                                                                 {
1284                                                                         $aTerms[] = "address_rank <= ".$this->iMaxAddressRank;
1285                                                                 }
1286                                                         }
1287                                                         if ($aSearch['fLon'] && $aSearch['fLat'])
1288                                                         {
1289                                                                 $aTerms[] = "ST_DWithin(centroid, ST_SetSRID(ST_Point(".$aSearch['fLon'].",".$aSearch['fLat']."),4326), ".$aSearch['fRadius'].")";
1290                                                                 $aOrder[] = "ST_Distance(centroid, ST_SetSRID(ST_Point(".$aSearch['fLon'].",".$aSearch['fLat']."),4326)) ASC";
1291                                                         }
1292                                                         if (sizeof($this->aExcludePlaceIDs))
1293                                                         {
1294                                                                 $aTerms[] = "place_id not in (".join(',',$this->aExcludePlaceIDs).")";
1295                                                         }
1296                                                         if ($sCountryCodesSQL)
1297                                                         {
1298                                                                 $aTerms[] = "country_code in ($sCountryCodesSQL)";
1299                                                         }
1300
1301                                                         if ($bBoundingBoxSearch) $aTerms[] = "centroid && $this->sViewboxSmallSQL";
1302                                                         if ($sNearPointSQL) $aOrder[] = "ST_Distance($sNearPointSQL, centroid) asc";
1303
1304                                                         if ($aSearch['sHouseNumber'])
1305                                                         {
1306                                                                 $sImportanceSQL = '- abs(26 - address_rank) + 3';
1307                                                         }
1308                                                         else
1309                                                         {
1310                                                                 $sImportanceSQL = '(case when importance = 0 OR importance IS NULL then 0.75-(search_rank::float/40) else importance end)';
1311                                                         }
1312                                                         if ($this->sViewboxSmallSQL) $sImportanceSQL .= " * case when ST_Contains($this->sViewboxSmallSQL, centroid) THEN 1 ELSE 0.5 END";
1313                                                         if ($this->sViewboxLargeSQL) $sImportanceSQL .= " * case when ST_Contains($this->sViewboxLargeSQL, centroid) THEN 1 ELSE 0.5 END";
1314
1315                                                         $aOrder[] = "$sImportanceSQL DESC";
1316                                                         if (sizeof($aSearch['aFullNameAddress']))
1317                                                         {
1318                                                                 $sExactMatchSQL = '(select count(*) from (select unnest(ARRAY['.join($aSearch['aFullNameAddress'],",").']) INTERSECT select unnest(nameaddress_vector))s) as exactmatch';
1319                                                                 $aOrder[] = 'exactmatch DESC';
1320                                                         } else {
1321                                                                 $sExactMatchSQL = '0::int as exactmatch';
1322                                                         }
1323
1324                                                         if (sizeof($aTerms))
1325                                                         {
1326                                                                 $sSQL = "select place_id, ";
1327                                                                 $sSQL .= $sExactMatchSQL;
1328                                                                 $sSQL .= " from search_name";
1329                                                                 $sSQL .= " where ".join(' and ',$aTerms);
1330                                                                 $sSQL .= " order by ".join(', ',$aOrder);
1331                                                                 if ($aSearch['sHouseNumber'] || $aSearch['sClass'])
1332                                                                         $sSQL .= " limit 20";
1333                                                                 elseif (!sizeof($aSearch['aName']) && !sizeof($aSearch['aAddress']) && $aSearch['sClass'])
1334                                                                         $sSQL .= " limit 1";
1335                                                                 else
1336                                                                         $sSQL .= " limit ".$this->iLimit;
1337
1338                                                                 if (CONST_Debug) { var_dump($sSQL); }
1339                                                                 $aViewBoxPlaceIDs = $this->oDB->getAll($sSQL);
1340                                                                 if (PEAR::IsError($aViewBoxPlaceIDs))
1341                                                                 {
1342                                                                         failInternalError("Could not get places for search terms.", $sSQL, $aViewBoxPlaceIDs);
1343                                                                 }
1344                                                                 //var_dump($aViewBoxPlaceIDs);
1345                                                                 // Did we have an viewbox matches?
1346                                                                 $aPlaceIDs = array();
1347                                                                 $bViewBoxMatch = false;
1348                                                                 foreach($aViewBoxPlaceIDs as $aViewBoxRow)
1349                                                                 {
1350                                                                         //if ($bViewBoxMatch == 1 && $aViewBoxRow['in_small'] == 'f') break;
1351                                                                         //if ($bViewBoxMatch == 2 && $aViewBoxRow['in_large'] == 'f') break;
1352                                                                         //if ($aViewBoxRow['in_small'] == 't') $bViewBoxMatch = 1;
1353                                                                         //else if ($aViewBoxRow['in_large'] == 't') $bViewBoxMatch = 2;
1354                                                                         $aPlaceIDs[] = $aViewBoxRow['place_id'];
1355                                                                         $this->exactMatchCache[$aViewBoxRow['place_id']] = $aViewBoxRow['exactmatch'];
1356                                                                 }
1357                                                         }
1358                                                         //var_Dump($aPlaceIDs);
1359                                                         //exit;
1360
1361                                                         if ($aSearch['sHouseNumber'] && sizeof($aPlaceIDs))
1362                                                         {
1363                                                                 $aRoadPlaceIDs = $aPlaceIDs;
1364                                                                 $sPlaceIDs = join(',',$aPlaceIDs);
1365
1366                                                                 // Now they are indexed look for a house attached to a street we found
1367                                                                 $sHouseNumberRegex = '\\\\m'.$aSearch['sHouseNumber'].'\\\\M';
1368                                                                 $sSQL = "select place_id from placex where parent_place_id in (".$sPlaceIDs.") and transliteration(housenumber) ~* E'".$sHouseNumberRegex."'";
1369                                                                 if (sizeof($this->aExcludePlaceIDs))
1370                                                                 {
1371                                                                         $sSQL .= " and place_id not in (".join(',',$this->aExcludePlaceIDs).")";
1372                                                                 }
1373                                                                 $sSQL .= " limit $this->iLimit";
1374                                                                 if (CONST_Debug) var_dump($sSQL);
1375                                                                 $aPlaceIDs = $this->oDB->getCol($sSQL);
1376
1377                                                                 // If not try the aux fallback table
1378                                                                 if (!sizeof($aPlaceIDs))
1379                                                                 {
1380                                                                         $sSQL = "select place_id from location_property_aux where parent_place_id in (".$sPlaceIDs.") and housenumber = '".pg_escape_string($aSearch['sHouseNumber'])."'";
1381                                                                         if (sizeof($this->aExcludePlaceIDs))
1382                                                                         {
1383                                                                                 $sSQL .= " and place_id not in (".join(',',$this->aExcludePlaceIDs).")";
1384                                                                         }
1385                                                                         //$sSQL .= " limit $this->iLimit";
1386                                                                         if (CONST_Debug) var_dump($sSQL);
1387                                                                         $aPlaceIDs = $this->oDB->getCol($sSQL);
1388                                                                 }
1389
1390                                                                 if (!sizeof($aPlaceIDs))
1391                                                                 {
1392                                                                         $sSQL = "select place_id from location_property_tiger where parent_place_id in (".$sPlaceIDs.") and housenumber = '".pg_escape_string($aSearch['sHouseNumber'])."'";
1393                                                                         if (sizeof($this->aExcludePlaceIDs))
1394                                                                         {
1395                                                                                 $sSQL .= " and place_id not in (".join(',',$this->aExcludePlaceIDs).")";
1396                                                                         }
1397                                                                         //$sSQL .= " limit $this->iLimit";
1398                                                                         if (CONST_Debug) var_dump($sSQL);
1399                                                                         $aPlaceIDs = $this->oDB->getCol($sSQL);
1400                                                                 }
1401
1402                                                                 // Fallback to the road
1403                                                                 if (!sizeof($aPlaceIDs) && preg_match('/[0-9]+/', $aSearch['sHouseNumber']))
1404                                                                 {
1405                                                                         $aPlaceIDs = $aRoadPlaceIDs;
1406                                                                 }
1407
1408                                                         }
1409
1410                                                         if ($aSearch['sClass'] && sizeof($aPlaceIDs))
1411                                                         {
1412                                                                 $sPlaceIDs = join(',',$aPlaceIDs);
1413                                                                 $aClassPlaceIDs = array();
1414
1415                                                                 if (!$aSearch['sOperator'] || $aSearch['sOperator'] == 'name')
1416                                                                 {
1417                                                                         // If they were searching for a named class (i.e. 'Kings Head pub') then we might have an extra match
1418                                                                         $sSQL = "select place_id from placex where place_id in ($sPlaceIDs) and class='".$aSearch['sClass']."' and type='".$aSearch['sType']."'";
1419                                                                         $sSQL .= " and linked_place_id is null";
1420                                                                         if ($sCountryCodesSQL) $sSQL .= " and calculated_country_code in ($sCountryCodesSQL)";
1421                                                                         $sSQL .= " order by rank_search asc limit $this->iLimit";
1422                                                                         if (CONST_Debug) var_dump($sSQL);
1423                                                                         $aClassPlaceIDs = $this->oDB->getCol($sSQL);
1424                                                                 }
1425
1426                                                                 if (!$aSearch['sOperator'] || $aSearch['sOperator'] == 'near') // & in
1427                                                                 {
1428                                                                         $sSQL = "select count(*) from pg_tables where tablename = 'place_classtype_".$aSearch['sClass']."_".$aSearch['sType']."'";
1429                                                                         $bCacheTable = $this->oDB->getOne($sSQL);
1430
1431                                                                         $sSQL = "select min(rank_search) from placex where place_id in ($sPlaceIDs)";
1432
1433                                                                         if (CONST_Debug) var_dump($sSQL);
1434                                                                         $this->iMaxRank = ((int)$this->oDB->getOne($sSQL));
1435
1436                                                                         // For state / country level searches the normal radius search doesn't work very well
1437                                                                         $sPlaceGeom = false;
1438                                                                         if ($this->iMaxRank < 9 && $bCacheTable)
1439                                                                         {
1440                                                                                 // Try and get a polygon to search in instead
1441                                                                                 $sSQL = "select geometry from placex where place_id in ($sPlaceIDs) and rank_search < $this->iMaxRank + 5 and st_geometrytype(geometry) in ('ST_Polygon','ST_MultiPolygon') order by rank_search asc limit 1";
1442                                                                                 if (CONST_Debug) var_dump($sSQL);
1443                                                                                 $sPlaceGeom = $this->oDB->getOne($sSQL);
1444                                                                         }
1445
1446                                                                         if ($sPlaceGeom)
1447                                                                         {
1448                                                                                 $sPlaceIDs = false;
1449                                                                         }
1450                                                                         else
1451                                                                         {
1452                                                                                 $this->iMaxRank += 5;
1453                                                                                 $sSQL = "select place_id from placex where place_id in ($sPlaceIDs) and rank_search < $this->iMaxRank";
1454                                                                                 if (CONST_Debug) var_dump($sSQL);
1455                                                                                 $aPlaceIDs = $this->oDB->getCol($sSQL);
1456                                                                                 $sPlaceIDs = join(',',$aPlaceIDs);
1457                                                                         }
1458
1459                                                                         if ($sPlaceIDs || $sPlaceGeom)
1460                                                                         {
1461
1462                                                                                 $fRange = 0.01;
1463                                                                                 if ($bCacheTable)
1464                                                                                 {
1465                                                                                         // More efficient - can make the range bigger
1466                                                                                         $fRange = 0.05;
1467
1468                                                                                         $sOrderBySQL = '';
1469                                                                                         if ($sNearPointSQL) $sOrderBySQL = "ST_Distance($sNearPointSQL, l.centroid)";
1470                                                                                         else if ($sPlaceIDs) $sOrderBySQL = "ST_Distance(l.centroid, f.geometry)";
1471                                                                                         else if ($sPlaceGeom) $sOrderBysSQL = "ST_Distance(st_centroid('".$sPlaceGeom."'), l.centroid)";
1472
1473                                                                                         $sSQL = "select distinct l.place_id".($sOrderBySQL?','.$sOrderBySQL:'')." from place_classtype_".$aSearch['sClass']."_".$aSearch['sType']." as l";
1474                                                                                         if ($sCountryCodesSQL) $sSQL .= " join placex as lp using (place_id)";
1475                                                                                         if ($sPlaceIDs)
1476                                                                                         {
1477                                                                                                 $sSQL .= ",placex as f where ";
1478                                                                                                 $sSQL .= "f.place_id in ($sPlaceIDs) and ST_DWithin(l.centroid, f.centroid, $fRange) ";
1479                                                                                         }
1480                                                                                         if ($sPlaceGeom)
1481                                                                                         {
1482                                                                                                 $sSQL .= " where ";
1483                                                                                                 $sSQL .= "ST_Contains('".$sPlaceGeom."', l.centroid) ";
1484                                                                                         }
1485                                                                                         if (sizeof($this->aExcludePlaceIDs))
1486                                                                                         {
1487                                                                                                 $sSQL .= " and l.place_id not in (".join(',',$this->aExcludePlaceIDs).")";
1488                                                                                         }
1489                                                                                         if ($sCountryCodesSQL) $sSQL .= " and lp.calculated_country_code in ($sCountryCodesSQL)";
1490                                                                                         if ($sOrderBySQL) $sSQL .= "order by ".$sOrderBySQL." asc";
1491                                                                                         if ($this->iOffset) $sSQL .= " offset $this->iOffset";
1492                                                                                         $sSQL .= " limit $this->iLimit";
1493                                                                                         if (CONST_Debug) var_dump($sSQL);
1494                                                                                         $aClassPlaceIDs = array_merge($aClassPlaceIDs, $this->oDB->getCol($sSQL));
1495                                                                                 }
1496                                                                                 else
1497                                                                                 {
1498                                                                                         if (isset($aSearch['fRadius']) && $aSearch['fRadius']) $fRange = $aSearch['fRadius'];
1499
1500                                                                                         $sOrderBySQL = '';
1501                                                                                         if ($sNearPointSQL) $sOrderBySQL = "ST_Distance($sNearPointSQL, l.geometry)";
1502                                                                                         else $sOrderBySQL = "ST_Distance(l.geometry, f.geometry)";
1503
1504                                                                                         $sSQL = "select distinct l.place_id".($sOrderBysSQL?','.$sOrderBysSQL:'')." from placex as l,placex as f where ";
1505                                                                                         $sSQL .= "f.place_id in ( $sPlaceIDs) and ST_DWithin(l.geometry, f.centroid, $fRange) ";
1506                                                                                         $sSQL .= "and l.class='".$aSearch['sClass']."' and l.type='".$aSearch['sType']."' ";
1507                                                                                         if (sizeof($this->aExcludePlaceIDs))
1508                                                                                         {
1509                                                                                                 $sSQL .= " and l.place_id not in (".join(',',$this->aExcludePlaceIDs).")";
1510                                                                                         }
1511                                                                                         if ($sCountryCodesSQL) $sSQL .= " and l.calculated_country_code in ($sCountryCodesSQL)";
1512                                                                                         if ($sOrderBy) $sSQL .= "order by ".$OrderBysSQL." asc";
1513                                                                                         if ($this->iOffset) $sSQL .= " offset $this->iOffset";
1514                                                                                         $sSQL .= " limit $this->iLimit";
1515                                                                                         if (CONST_Debug) var_dump($sSQL);
1516                                                                                         $aClassPlaceIDs = array_merge($aClassPlaceIDs, $this->oDB->getCol($sSQL));
1517                                                                                 }
1518                                                                         }
1519                                                                 }
1520
1521                                                                 $aPlaceIDs = $aClassPlaceIDs;
1522
1523                                                         }
1524
1525                                                 }
1526
1527                                                 if (PEAR::IsError($aPlaceIDs))
1528                                                 {
1529                                                         failInternalError("Could not get place IDs from tokens." ,$sSQL, $aPlaceIDs);
1530                                                 }
1531
1532                                                 if (CONST_Debug) { echo "<br><b>Place IDs:</b> "; var_Dump($aPlaceIDs); }
1533
1534                                                 foreach($aPlaceIDs as $iPlaceID)
1535                                                 {
1536                                                         $aResultPlaceIDs[$iPlaceID] = $iPlaceID;
1537                                                 }
1538                                                 if ($iQueryLoop > 20) break;
1539                                         }
1540
1541                                         if (isset($aResultPlaceIDs) && sizeof($aResultPlaceIDs) && ($this->iMinAddressRank != 0 || $this->iMaxAddressRank != 30))
1542                                         {
1543                                                 // Need to verify passes rank limits before dropping out of the loop (yuk!)
1544                                                 $sSQL = "select place_id from placex where place_id in (".join(',',$aResultPlaceIDs).") ";
1545                                                 $sSQL .= "and (placex.rank_address between $this->iMinAddressRank and $this->iMaxAddressRank ";
1546                                                 if (14 >= $this->iMinAddressRank && 14 <= $this->iMaxAddressRank) $sSQL .= " OR (extratags->'place') = 'city'";
1547                                                 if ($this->aAddressRankList) $sSQL .= " OR placex.rank_address in (".join(',',$this->aAddressRankList).")";
1548                                                 $sSQL .= ") UNION select place_id from location_property_tiger where place_id in (".join(',',$aResultPlaceIDs).") ";
1549                                                 $sSQL .= "and (30 between $this->iMinAddressRank and $this->iMaxAddressRank ";
1550                                                 if ($this->aAddressRankList) $sSQL .= " OR 30 in (".join(',',$this->aAddressRankList).")";
1551                                                 $sSQL .= ")";
1552                                                 if (CONST_Debug) var_dump($sSQL);
1553                                                 $aResultPlaceIDs = $this->oDB->getCol($sSQL);
1554                                         }
1555
1556                                         //exit;
1557                                         if (isset($aResultPlaceIDs) && sizeof($aResultPlaceIDs)) break;
1558                                         if ($iGroupLoop > 4) break;
1559                                         if ($iQueryLoop > 30) break;
1560                                 }
1561
1562                                 // Did we find anything?
1563                                 if (isset($aResultPlaceIDs) && sizeof($aResultPlaceIDs))
1564                                 {
1565                                         $aSearchResults = $this->getDetails($aResultPlaceIDs);
1566                                 }
1567
1568                         }
1569                         else
1570                         {
1571                                 // Just interpret as a reverse geocode
1572                                 $iPlaceID = geocodeReverse((float)$this->aNearPoint[0], (float)$this->aNearPoint[1]);
1573                                 if ($iPlaceID)
1574                                         $aSearchResults = $this->getDetails(array($iPlaceID));
1575                                 else
1576                                         $aSearchResults = array();
1577                         }
1578
1579                         // No results? Done
1580                         if (!sizeof($aSearchResults))
1581                         {
1582                                 if ($this->bFallback)
1583                                 {
1584                                         if ($this->fallbackStructuredQuery())
1585                                         {
1586                                                 return $this->lookup();
1587                                         }
1588                                 }
1589
1590                                 return array();
1591                         }
1592
1593                         $aClassType = getClassTypesWithImportance();
1594                         $aRecheckWords = preg_split('/\b[\s,\\-]*/u',$sQuery);
1595                         foreach($aRecheckWords as $i => $sWord)
1596                         {
1597                                 if (!preg_match('/\pL/', $sWord)) unset($aRecheckWords[$i]);
1598                         }
1599
1600             if (CONST_Debug) { echo '<i>Recheck words:<\i>'; var_dump($aRecheckWords); }
1601
1602                         foreach($aSearchResults as $iResNum => $aResult)
1603                         {
1604                                 if (CONST_Search_AreaPolygons)
1605                                 {
1606                                         // Get the bounding box and outline polygon
1607                                         $sSQL = "select place_id,0 as numfeatures,st_area(geometry) as area,";
1608                                         $sSQL .= "ST_Y(centroid) as centrelat,ST_X(centroid) as centrelon,";
1609                                         $sSQL .= "ST_YMin(geometry) as minlat,ST_YMax(geometry) as maxlat,";
1610                                         $sSQL .= "ST_XMin(geometry) as minlon,ST_XMax(geometry) as maxlon";
1611                                         if ($this->bIncludePolygonAsGeoJSON) $sSQL .= ",ST_AsGeoJSON(geometry) as asgeojson";
1612                                         if ($this->bIncludePolygonAsKML) $sSQL .= ",ST_AsKML(geometry) as askml";
1613                                         if ($this->bIncludePolygonAsSVG) $sSQL .= ",ST_AsSVG(geometry) as assvg";
1614                                         if ($this->bIncludePolygonAsText || $this->bIncludePolygonAsPoints) $sSQL .= ",ST_AsText(geometry) as astext";
1615                                         $sSQL .= " from placex where place_id = ".$aResult['place_id'];
1616                                         $aPointPolygon = $this->oDB->getRow($sSQL);
1617                                         if (PEAR::IsError($aPointPolygon))
1618                                         {
1619                                                 failInternalError("Could not get outline.", $sSQL, $aPointPolygon);
1620                                         }
1621
1622                                         if ($aPointPolygon['place_id'])
1623                                         {
1624                                                 if ($this->bIncludePolygonAsGeoJSON) $aResult['asgeojson'] = $aPointPolygon['asgeojson'];
1625                                                 if ($this->bIncludePolygonAsKML) $aResult['askml'] = $aPointPolygon['askml'];
1626                                                 if ($this->bIncludePolygonAsSVG) $aResult['assvg'] = $aPointPolygon['assvg'];
1627                                                 if ($this->bIncludePolygonAsText) $aResult['astext'] = $aPointPolygon['astext'];
1628
1629                                                 if ($aPointPolygon['centrelon'] !== null && $aPointPolygon['centrelat'] !== null )
1630                                                 {
1631                                                         $aResult['lat'] = $aPointPolygon['centrelat'];
1632                                                         $aResult['lon'] = $aPointPolygon['centrelon'];
1633                                                 }
1634
1635                                                 if ($this->bIncludePolygonAsPoints)
1636                                                 {
1637                                                         // Translate geometry string to point array
1638                                                         if (preg_match('#POLYGON\\(\\(([- 0-9.,]+)#',$aPointPolygon['astext'],$aMatch))
1639                                                         {
1640                                                                 preg_match_all('/(-?[0-9.]+) (-?[0-9.]+)/',$aMatch[1],$aPolyPoints,PREG_SET_ORDER);
1641                                                         }
1642                                                         elseif (preg_match('#MULTIPOLYGON\\(\\(\\(([- 0-9.,]+)#',$aPointPolygon['astext'],$aMatch))
1643                                                         {
1644                                                                 preg_match_all('/(-?[0-9.]+) (-?[0-9.]+)/',$aMatch[1],$aPolyPoints,PREG_SET_ORDER);
1645                                                         }
1646                                                         elseif (preg_match('#POINT\\((-?[0-9.]+) (-?[0-9.]+)\\)#',$aPointPolygon['astext'],$aMatch))
1647                                                         {
1648                                                                 $fRadius = 0.01;
1649                                                                 $iSteps = ($fRadius * 40000)^2;
1650                                                                 $fStepSize = (2*pi())/$iSteps;
1651                                                                 $aPolyPoints = array();
1652                                                                 for($f = 0; $f < 2*pi(); $f += $fStepSize)
1653                                                                 {
1654                                                                         $aPolyPoints[] = array('',$aMatch[1]+($fRadius*sin($f)),$aMatch[2]+($fRadius*cos($f)));
1655                                                                 }
1656                                                                 $aPointPolygon['minlat'] = $aPointPolygon['minlat'] - $fRadius;
1657                                                                 $aPointPolygon['maxlat'] = $aPointPolygon['maxlat'] + $fRadius;
1658                                                                 $aPointPolygon['minlon'] = $aPointPolygon['minlon'] - $fRadius;
1659                                                                 $aPointPolygon['maxlon'] = $aPointPolygon['maxlon'] + $fRadius;
1660                                                         }
1661                                                 }
1662
1663                                                 // Output data suitable for display (points and a bounding box)
1664                                                 if ($this->bIncludePolygonAsPoints && isset($aPolyPoints))
1665                                                 {
1666                                                         $aResult['aPolyPoints'] = array();
1667                                                         foreach($aPolyPoints as $aPoint)
1668                                                         {
1669                                                                 $aResult['aPolyPoints'][] = array($aPoint[1], $aPoint[2]);
1670                                                         }
1671                                                 }
1672                                                 $aResult['aBoundingBox'] = array($aPointPolygon['minlat'],$aPointPolygon['maxlat'],$aPointPolygon['minlon'],$aPointPolygon['maxlon']);
1673                                         }
1674                                 }
1675
1676                                 if ($aResult['extra_place'] == 'city')
1677                                 {
1678                                         $aResult['class'] = 'place';
1679                                         $aResult['type'] = 'city';
1680                                         $aResult['rank_search'] = 16;
1681                                 }
1682
1683                                 if (!isset($aResult['aBoundingBox']))
1684                                 {
1685                                         // Default
1686                                         $fDiameter = 0.0001;
1687
1688                                         if (isset($aClassType[$aResult['class'].':'.$aResult['type'].':'.$aResult['admin_level']]['defdiameter'])
1689                                                         && $aClassType[$aResult['class'].':'.$aResult['type'].':'.$aResult['admin_level']]['defdiameter'])
1690                                         {
1691                                                 $fDiameter = $aClassType[$aResult['class'].':'.$aResult['type'].':'.$aResult['admin_level']]['defzoom'];
1692                                         }
1693                                         elseif (isset($aClassType[$aResult['class'].':'.$aResult['type']]['defdiameter'])
1694                                                         && $aClassType[$aResult['class'].':'.$aResult['type']]['defdiameter'])
1695                                         {
1696                                                 $fDiameter = $aClassType[$aResult['class'].':'.$aResult['type']]['defdiameter'];
1697                                         }
1698                                         $fRadius = $fDiameter / 2;
1699
1700                                         $iSteps = max(8,min(100,$fRadius * 3.14 * 100000));
1701                                         $fStepSize = (2*pi())/$iSteps;
1702                                         $aPolyPoints = array();
1703                                         for($f = 0; $f < 2*pi(); $f += $fStepSize)
1704                                         {
1705                                                 $aPolyPoints[] = array('',$aResult['lon']+($fRadius*sin($f)),$aResult['lat']+($fRadius*cos($f)));
1706                                         }
1707                                         $aPointPolygon['minlat'] = $aResult['lat'] - $fRadius;
1708                                         $aPointPolygon['maxlat'] = $aResult['lat'] + $fRadius;
1709                                         $aPointPolygon['minlon'] = $aResult['lon'] - $fRadius;
1710                                         $aPointPolygon['maxlon'] = $aResult['lon'] + $fRadius;
1711
1712                                         // Output data suitable for display (points and a bounding box)
1713                                         if ($this->bIncludePolygonAsPoints)
1714                                         {
1715                                                 $aResult['aPolyPoints'] = array();
1716                                                 foreach($aPolyPoints as $aPoint)
1717                                                 {
1718                                                         $aResult['aPolyPoints'][] = array($aPoint[1], $aPoint[2]);
1719                                                 }
1720                                         }
1721                                         $aResult['aBoundingBox'] = array((string)$aPointPolygon['minlat'],(string)$aPointPolygon['maxlat'],(string)$aPointPolygon['minlon'],(string)$aPointPolygon['maxlon']);
1722                                 }
1723
1724                                 // Is there an icon set for this type of result?
1725                                 if (isset($aClassType[$aResult['class'].':'.$aResult['type']]['icon'])
1726                                                 && $aClassType[$aResult['class'].':'.$aResult['type']]['icon'])
1727                                 {
1728                                         $aResult['icon'] = CONST_Website_BaseURL.'images/mapicons/'.$aClassType[$aResult['class'].':'.$aResult['type']]['icon'].'.p.20.png';
1729                                 }
1730
1731                                 if (isset($aClassType[$aResult['class'].':'.$aResult['type'].':'.$aResult['admin_level']]['label'])
1732                                                 && $aClassType[$aResult['class'].':'.$aResult['type'].':'.$aResult['admin_level']]['label'])
1733                                 {
1734                                         $aResult['label'] = $aClassType[$aResult['class'].':'.$aResult['type'].':'.$aResult['admin_level']]['label'];
1735                                 }
1736                                 elseif (isset($aClassType[$aResult['class'].':'.$aResult['type']]['label'])
1737                                                 && $aClassType[$aResult['class'].':'.$aResult['type']]['label'])
1738                                 {
1739                                         $aResult['label'] = $aClassType[$aResult['class'].':'.$aResult['type']]['label'];
1740                                 }
1741
1742                                 if ($this->bIncludeAddressDetails)
1743                                 {
1744                                         $aResult['address'] = getAddressDetails($this->oDB, $sLanguagePrefArraySQL, $aResult['place_id'], $aResult['country_code']);
1745                                         if ($aResult['extra_place'] == 'city' && !isset($aResult['address']['city']))
1746                                         {
1747                                                 $aResult['address'] = array_merge(array('city' => array_shift(array_values($aResult['address']))), $aResult['address']);
1748                                         }
1749                                 }
1750
1751                                 // Adjust importance for the number of exact string matches in the result
1752                                 $aResult['importance'] = max(0.001,$aResult['importance']);
1753                                 $iCountWords = 0;
1754                                 $sAddress = $aResult['langaddress'];
1755                                 foreach($aRecheckWords as $i => $sWord)
1756                                 {
1757                                         if (stripos($sAddress, $sWord)!==false)
1758                                         {
1759                                                 $iCountWords++;
1760                                                 if (preg_match("/(^|,)\s*".preg_quote($sWord, '/')."\s*(,|$)/", $sAddress)) $iCountWords += 0.1;
1761                                         }
1762                                 }
1763
1764                                 $aResult['importance'] = $aResult['importance'] + ($iCountWords*0.1); // 0.1 is a completely arbitrary number but something in the range 0.1 to 0.5 would seem right
1765
1766                                 $aResult['name'] = $aResult['langaddress'];
1767                                 // secondary ordering (for results with same importance (the smaller the better):
1768                                 //   - approximate importance of address parts
1769                                 $aResult['foundorder'] = -$aResult['addressimportance']/10;
1770                                 //   - number of exact matches from the query
1771                                 if (isset($this->exactMatchCache[$aResult['place_id']]))
1772                                         $aResult['foundorder'] -= $this->exactMatchCache[$aResult['place_id']];
1773                                 else if (isset($this->exactMatchCache[$aResult['parent_place_id']]))
1774                                         $aResult['foundorder'] -= $this->exactMatchCache[$aResult['parent_place_id']];
1775                                 //  - importance of the class/type
1776                                 if (isset($aClassType[$aResult['class'].':'.$aResult['type']]['importance'])
1777                                         && $aClassType[$aResult['class'].':'.$aResult['type']]['importance'])
1778                                 {
1779                                         $aResult['foundorder'] = $aResult['foundorder'] + 0.000001 * $aClassType[$aResult['class'].':'.$aResult['type']]['importance'];
1780                                 }
1781                                 else
1782                                 {
1783                                         $aResult['foundorder'] = $aResult['foundorder'] + 0.001;
1784                                 }
1785                                 $aSearchResults[$iResNum] = $aResult;
1786                         }
1787                         uasort($aSearchResults, 'byImportance');
1788
1789                         $aOSMIDDone = array();
1790                         $aClassTypeNameDone = array();
1791                         $aToFilter = $aSearchResults;
1792                         $aSearchResults = array();
1793
1794                         $bFirst = true;
1795                         foreach($aToFilter as $iResNum => $aResult)
1796                         {
1797                                 $this->aExcludePlaceIDs[$aResult['place_id']] = $aResult['place_id'];
1798                                 if ($bFirst)
1799                                 {
1800                                         $fLat = $aResult['lat'];
1801                                         $fLon = $aResult['lon'];
1802                                         if (isset($aResult['zoom'])) $iZoom = $aResult['zoom'];
1803                                         $bFirst = false;
1804                                 }
1805                                 if (!$this->bDeDupe || (!isset($aOSMIDDone[$aResult['osm_type'].$aResult['osm_id']])
1806                                                         && !isset($aClassTypeNameDone[$aResult['osm_type'].$aResult['class'].$aResult['type'].$aResult['name'].$aResult['admin_level']])))
1807                                 {
1808                                         $aOSMIDDone[$aResult['osm_type'].$aResult['osm_id']] = true;
1809                                         $aClassTypeNameDone[$aResult['osm_type'].$aResult['class'].$aResult['type'].$aResult['name'].$aResult['admin_level']] = true;
1810                                         $aSearchResults[] = $aResult;
1811                                 }
1812
1813                                 // Absolute limit on number of results
1814                                 if (sizeof($aSearchResults) >= $this->iFinalLimit) break;
1815                         }
1816
1817                         return $aSearchResults;
1818
1819                 } // end lookup()
1820
1821
1822         } // end class
1823