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