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