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