From a50b0faf4abfb1bba68e03d843c58f07f842cf12 Mon Sep 17 00:00:00 2001 From: Dhruv Goel Date: Fri, 4 May 2012 18:39:41 +0530 Subject: [feature/sphinx-fulltext-search] MOD by naderman installed in phpbb PHPBB3-10946 --- phpBB/includes/functions_sphinx.php | 507 ++++++++++++ phpBB/includes/search/fulltext_sphinx.php | 1166 +++++++++++++++++++++++++++ phpBB/includes/sphinxapi-0.9.8.php | 1202 ++++++++++++++++++++++++++++ phpBB/language/en/mods/fulltext_sphinx.php | 65 ++ 4 files changed, 2940 insertions(+) create mode 100644 phpBB/includes/functions_sphinx.php create mode 100644 phpBB/includes/search/fulltext_sphinx.php create mode 100644 phpBB/includes/sphinxapi-0.9.8.php create mode 100644 phpBB/language/en/mods/fulltext_sphinx.php (limited to 'phpBB') diff --git a/phpBB/includes/functions_sphinx.php b/phpBB/includes/functions_sphinx.php new file mode 100644 index 0000000000..976f93f77c --- /dev/null +++ b/phpBB/includes/functions_sphinx.php @@ -0,0 +1,507 @@ +read($filename); + } + } + + /** + * Get a section object by its name + * + * @param string $name The name of the section that shall be returned + * @return sphinx_config_section The section object or null if none was found + */ + function &get_section_by_name($name) + { + for ($i = 0, $n = sizeof($this->sections); $i < $n; $i++) + { + // make sure this is really a section object and not a comment + if (is_a($this->sections[$i], 'sphinx_config_section') && $this->sections[$i]->get_name() == $name) + { + return $this->sections[$i]; + } + } + $null = null; + return $null; + } + + /** + * Appends a new empty section to the end of the config + * + * @param string $name The name for the new section + * @return sphinx_config_section The newly created section object + */ + function &add_section($name) + { + $this->sections[] = new sphinx_config_section($name, ''); + return $this->sections[sizeof($this->sections) - 1]; + } + + /** + * Parses the config file at the given path, which is stored in $this->loaded for later use + * + * @param string $filename The path to the config file + */ + function read($filename) + { + // split the file into lines, we'll process it line by line + $config_file = file($filename); + + $this->sections = array(); + + $section = null; + $found_opening_bracket = false; + $in_value = false; + + foreach ($config_file as $i => $line) + { + // if the value of a variable continues to the next line because the line break was escaped + // then we don't trim leading space but treat it as a part of the value + if ($in_value) + { + $line = rtrim($line); + } + else + { + $line = trim($line); + } + + // if we're not inside a section look for one + if (!$section) + { + // add empty lines and comments as comment objects to the section list + // that way they're not deleted when reassembling the file from the sections + if (!$line || $line[0] == '#') + { + $this->sections[] = new sphinx_config_comment($config_file[$i]); + continue; + } + else + { + // otherwise we scan the line reading the section name until we find + // an opening curly bracket or a comment + $section_name = ''; + $section_name_comment = ''; + $found_opening_bracket = false; + for ($j = 0, $n = strlen($line); $j < $n; $j++) + { + if ($line[$j] == '#') + { + $section_name_comment = substr($line, $j); + break; + } + + if ($found_opening_bracket) + { + continue; + } + + if ($line[$j] == '{') + { + $found_opening_bracket = true; + continue; + } + + $section_name .= $line[$j]; + } + + // and then we create the new section object + $section_name = trim($section_name); + $section = new sphinx_config_section($section_name, $section_name_comment); + } + } + else // if we're looking for variables inside a section + { + $skip_first = false; + + // if we're not in a value continuing over the line feed + if (!$in_value) + { + // then add empty lines and comments as comment objects to the variable list + // of this section so they're not deleted on reassembly + if (!$line || $line[0] == '#') + { + $section->add_variable(new sphinx_config_comment($config_file[$i])); + continue; + } + + // as long as we haven't yet actually found an opening bracket for this section + // we treat everything as comments so it's not deleted either + if (!$found_opening_bracket) + { + if ($line[0] == '{') + { + $skip_first = true; + $line = substr($line, 1); + $found_opening_bracket = true; + } + else + { + $section->add_variable(new sphinx_config_comment($config_file[$i])); + continue; + } + } + } + + // if we did not find a comment in this line or still add to the previous line's value ... + if ($line || $in_value) + { + if (!$in_value) + { + $name = ''; + $value = ''; + $comment = ''; + $found_assignment = false; + } + $in_value = false; + $end_section = false; + + // ... then we should prase this line char by char: + // - first there's the variable name + // - then an equal sign + // - the variable value + // - possibly a backslash before the linefeed in this case we need to continue + // parsing the value in the next line + // - a # indicating that the rest of the line is a comment + // - a closing curly bracket indicating the end of this section + for ($j = 0, $n = strlen($line); $j < $n; $j++) + { + if ($line[$j] == '#') + { + $comment = substr($line, $j); + break; + } + else if ($line[$j] == '}') + { + $comment = substr($line, $j + 1); + $end_section = true; + break; + } + else if (!$found_assignment) + { + if ($line[$j] == '=') + { + $found_assignment = true; + } + else + { + $name .= $line[$j]; + } + } + else + { + if ($line[$j] == '\\' && $j == $n - 1) + { + $value .= "\n"; + $in_value = true; + continue 2; // go to the next line and keep processing the value in there + } + $value .= $line[$j]; + } + } + + // if a name and an equal sign were found then we have append a new variable object to the section + if ($name && $found_assignment) + { + $section->add_variable(new sphinx_config_variable(trim($name), trim($value), ($end_section) ? '' : $comment)); + continue; + } + + // if we found a closing curly bracket this section has been completed and we can append it to the section list + // and continue with looking for the next section + if ($end_section) + { + $section->set_end_comment($comment); + $this->sections[] = $section; + $section = null; + continue; + } + } + + // if we did not find anything meaningful up to here, then just treat it as a comment + $comment = ($skip_first) ? "\t" . substr(ltrim($config_file[$i]), 1) : $config_file[$i]; + $section->add_variable(new sphinx_config_comment($comment)); + } + } + + // keep the filename for later use + $this->loaded = $filename; + } + + /** + * Writes the config data into a file + * + * @param string $filename The optional filename into which the config data shall be written. + * If it's not specified it will be written into the file that the config + * was originally read from. + */ + function write($filename = false) + { + if ($filename === false && $this->loaded) + { + $filename = $this->loaded; + } + + $data = ""; + foreach ($this->sections as $section) + { + $data .= $section->to_string(); + } + + $fp = fopen($filename, 'wb'); + fwrite($fp, $data); + fclose($fp); + } +} + +/** +* sphinx_config_section +* Represents a single section inside the sphinx configuration +*/ +class sphinx_config_section +{ + var $name; + var $comment; + var $end_comment; + var $variables = array(); + + /** + * Construct a new section + * + * @param string $name Name of the section + * @param string $comment Comment that should be appended after the name in the + * textual format. + */ + function sphinx_config_section($name, $comment) + { + $this->name = $name; + $this->comment = $comment; + $this->end_comment = ''; + } + + /** + * Add a variable object to the list of variables in this section + * + * @param sphinx_config_variable $variable The variable object + */ + function add_variable($variable) + { + $this->variables[] = $variable; + } + + /** + * Adds a comment after the closing bracket in the textual representation + */ + function set_end_comment($end_comment) + { + $this->end_comment = $end_comment; + } + + /** + * Getter for the name of this section + * + * @return string Section's name + */ + function get_name() + { + return $this->name; + } + + /** + * Get a variable object by its name + * + * @param string $name The name of the variable that shall be returned + * @return sphinx_config_section The first variable object from this section with the + * given name or null if none was found + */ + function &get_variable_by_name($name) + { + for ($i = 0, $n = sizeof($this->variables); $i < $n; $i++) + { + // make sure this is a variable object and not a comment + if (is_a($this->variables[$i], 'sphinx_config_variable') && $this->variables[$i]->get_name() == $name) + { + return $this->variables[$i]; + } + } + $null = null; + return $null; + } + + /** + * Deletes all variables with the given name + * + * @param string $name The name of the variable objects that are supposed to be removed + */ + function delete_variables_by_name($name) + { + for ($i = 0; $i < sizeof($this->variables); $i++) + { + // make sure this is a variable object and not a comment + if (is_a($this->variables[$i], 'sphinx_config_variable') && $this->variables[$i]->get_name() == $name) + { + array_splice($this->variables, $i, 1); + $i--; + } + } + } + + /** + * Create a new variable object and append it to the variable list of this section + * + * @param string $name The name for the new variable + * @param string $value The value for the new variable + * @return sphinx_config_variable Variable object that was created + */ + function &create_variable($name, $value) + { + $this->variables[] = new sphinx_config_variable($name, $value, ''); + return $this->variables[sizeof($this->variables) - 1]; + } + + /** + * Turns this object into a string which can be written to a config file + * + * @return string Config data in textual form, parsable for sphinx + */ + function to_string() + { + $content = $this->name . " " . $this->comment . "\n{\n"; + + // make sure we don't get too many newlines after the opening bracket + while (trim($this->variables[0]->to_string()) == "") + { + array_shift($this->variables); + } + + foreach ($this->variables as $variable) + { + $content .= $variable->to_string(); + } + $content .= '}' . $this->end_comment . "\n"; + + return $content; + } +} + +/** +* sphinx_config_variable +* Represents a single variable inside the sphinx configuration +*/ +class sphinx_config_variable +{ + var $name; + var $value; + var $comment; + + /** + * Constructs a new variable object + * + * @param string $name Name of the variable + * @param string $value Value of the variable + * @param string $comment Optional comment after the variable in the + * config file + */ + function sphinx_config_variable($name, $value, $comment) + { + $this->name = $name; + $this->value = $value; + $this->comment = $comment; + } + + /** + * Getter for the variable's name + * + * @return string The variable object's name + */ + function get_name() + { + return $this->name; + } + + /** + * Allows changing the variable's value + * + * @param string $value New value for this variable + */ + function set_value($value) + { + $this->value = $value; + } + + /** + * Turns this object into a string readable by sphinx + * + * @return string Config data in textual form + */ + function to_string() + { + return "\t" . $this->name . ' = ' . str_replace("\n", "\\\n", $this->value) . ' ' . $this->comment . "\n"; + } +} + + +/** +* sphinx_config_comment +* Represents a comment inside the sphinx configuration +*/ +class sphinx_config_comment +{ + var $exact_string; + + /** + * Create a new comment + * + * @param string $exact_string The content of the comment including newlines, leading whitespace, etc. + */ + function sphinx_config_comment($exact_string) + { + $this->exact_string = $exact_string; + } + + /** + * Simply returns the comment as it was created + * + * @return string The exact string that was specified in the constructor + */ + function to_string() + { + return $this->exact_string; + } +} + +?> \ No newline at end of file diff --git a/phpBB/includes/search/fulltext_sphinx.php b/phpBB/includes/search/fulltext_sphinx.php new file mode 100644 index 0000000000..e0c467df93 --- /dev/null +++ b/phpBB/includes/search/fulltext_sphinx.php @@ -0,0 +1,1166 @@ +id = $config['avatar_salt']; + $this->indexes = 'index_phpbb_' . $this->id . '_delta;index_phpbb_' . $this->id . '_main'; + + $this->sphinx = new SphinxClient (); + + if (!empty($config['fulltext_sphinx_configured'])) + { + if ($config['fulltext_sphinx_autorun'] && !file_exists($config['fulltext_sphinx_data_path'] . 'searchd.pid') && $this->index_created(true)) + { + $this->shutdown_searchd(); +// $cwd = getcwd(); +// chdir($config['fulltext_sphinx_bin_path']); + exec($config['fulltext_sphinx_bin_path'] . SEARCHD_NAME . ' --config ' . $config['fulltext_sphinx_config_path'] . 'sphinx.conf >> ' . $config['fulltext_sphinx_data_path'] . 'log/searchd-startup.log 2>&1 &'); +// chdir($cwd); + } + + // we only support localhost for now + $this->sphinx->SetServer('localhost', (isset($config['fulltext_sphinx_port']) && $config['fulltext_sphinx_port']) ? (int) $config['fulltext_sphinx_port'] : 3312); + } + + $config['fulltext_sphinx_min_word_len'] = 2; + $config['fulltext_sphinx_max_word_len'] = 400; + + $error = false; + } + + /** + * Checks permissions and paths, if everything is correct it generates the config file + */ + function init() + { + global $db, $user, $config; + + if ($db->sql_layer != 'mysql' && $db->sql_layer != 'mysql4' && $db->sql_layer != 'mysqli') + { + return $user->lang['FULLTEXT_SPHINX_WRONG_DATABASE']; + } + + if ($error = $this->config_updated()) + { + return $error; + } + + // move delta to main index each hour + set_config('search_gc', 3600); + + return false; + } + + function config_updated() + { + global $db, $user, $config, $phpbb_root_path, $phpEx; + + if ($config['fulltext_sphinx_autoconf']) + { + $paths = array('fulltext_sphinx_bin_path', 'fulltext_sphinx_config_path', 'fulltext_sphinx_data_path'); + + // check for completeness and add trailing slash if it's not present + foreach ($paths as $path) + { + if (empty($config[$path])) + { + return $user->lang['FULLTEXT_SPHINX_UNCONFIGURED']; + } + if ($config[$path] && substr($config[$path], -1) != '/') + { + set_config($path, $config[$path] . '/'); + } + } + } + + $executables = array( + $config['fulltext_sphinx_bin_path'] . INDEXER_NAME, + $config['fulltext_sphinx_bin_path'] . SEARCHD_NAME, + ); + + if ($config['fulltext_sphinx_autorun']) + { + foreach ($executables as $executable) + { + if (!file_exists($executable)) + { + return sprintf($user->lang['FULLTEXT_SPHINX_FILE_NOT_FOUND'], $executable); + } + + if (!function_exists('exec')) + { + return $user->lang['FULLTEXT_SPHINX_REQUIRES_EXEC']; + } + + $output = array(); + @exec($executable, $output); + + $output = implode("\n", $output); + if (strpos($output, 'Sphinx ') === false) + { + return sprintf($user->lang['FULLTEXT_SPHINX_FILE_NOT_EXECUTABLE'], $executable); + } + } + } + + $writable_paths = array( + $config['fulltext_sphinx_config_path'] => array('config' => 'fulltext_sphinx_autoconf', 'subdir' => false), + $config['fulltext_sphinx_data_path'] => array('config' => 'fulltext_sphinx_autorun', 'subdir' => 'log'), + $config['fulltext_sphinx_data_path'] . 'log/' => array('config' => 'fulltext_sphinx_autorun', 'subdir' => false), + ); + + foreach ($writable_paths as $path => $info) + { + if ($config[$info['config']]) + { + // make sure directory exists + // if we could drop the @ here and figure out whether the file really + // doesn't exist or whether open_basedir is in effect, would be nice + if (!@file_exists($path)) + { + return sprintf($user->lang['FULLTEXT_SPHINX_DIRECTORY_NOT_FOUND'], $path); + } + + // now check if it is writable by storing a simple file + $filename = $path . 'write_test'; + $fp = @fopen($filename, 'wb'); + if ($fp === false) + { + return sprintf($user->lang['FULLTEXT_SPHINX_FILE_NOT_WRITABLE'], $filename); + } + @fclose($fp); + + @unlink($filename); + + if ($info['subdir'] !== false) + { + if (!is_dir($path . $info['subdir'])) + { + mkdir($path . $info['subdir']); + } + } + } + } + + if ($config['fulltext_sphinx_autoconf']) + { + include ($phpbb_root_path . 'config.' . $phpEx); + + // now that we're sure everything was entered correctly, generate a config for the index + // we misuse the avatar_salt for this, as it should be unique ;-) + + if (!class_exists('sphinx_config')) + { + include($phpbb_root_path . 'includes/functions_sphinx.php'); + } + + if (!file_exists($config['fulltext_sphinx_config_path'] . 'sphinx.conf')) + { + $filename = $config['fulltext_sphinx_config_path'] . 'sphinx.conf'; + $fp = @fopen($filename, 'wb'); + if ($fp === false) + { + return sprintf($user->lang['FULLTEXT_SPHINX_FILE_NOT_WRITABLE'], $filename); + } + @fclose($fp); + } + + $config_object = new sphinx_config($config['fulltext_sphinx_config_path'] . 'sphinx.conf'); + + $config_data = array( + "source source_phpbb_{$this->id}_main" => array( + array('type', 'mysql'), + array('sql_host', $dbhost), + array('sql_user', $dbuser), + array('sql_pass', $dbpasswd), + array('sql_db', $dbname), + array('sql_port', $dbport), + array('sql_query_pre', 'SET NAMES utf8'), + array('sql_query_pre', 'REPLACE INTO ' . SPHINX_TABLE . ' SELECT 1, MAX(post_id) FROM ' . POSTS_TABLE . ''), + array('sql_query_range', 'SELECT MIN(post_id), MAX(post_id) FROM ' . POSTS_TABLE . ''), + array('sql_range_step', '5000'), + array('sql_query', 'SELECT + p.post_id AS id, + p.forum_id, + p.topic_id, + p.poster_id, + IF(p.post_id = t.topic_first_post_id, 1, 0) as topic_first_post, + p.post_time, + p.post_subject, + p.post_subject as title, + p.post_text as data, + t.topic_last_post_time, + 0 as deleted + FROM ' . POSTS_TABLE . ' p, ' . TOPICS_TABLE . ' t + WHERE + p.topic_id = t.topic_id + AND p.post_id >= $start AND p.post_id <= $end'), + array('sql_query_post', ''), + array('sql_query_post_index', 'REPLACE INTO ' . SPHINX_TABLE . ' ( counter_id, max_doc_id ) VALUES ( 1, $maxid )'), + array('sql_query_info', 'SELECT * FROM ' . POSTS_TABLE . ' WHERE post_id = $id'), + array('sql_attr_uint', 'forum_id'), + array('sql_attr_uint', 'topic_id'), + array('sql_attr_uint', 'poster_id'), + array('sql_attr_bool', 'topic_first_post'), + array('sql_attr_bool', 'deleted'), + array('sql_attr_timestamp' , 'post_time'), + array('sql_attr_timestamp' , 'topic_last_post_time'), + array('sql_attr_str2ordinal', 'post_subject'), + ), + "source source_phpbb_{$this->id}_delta : source_phpbb_{$this->id}_main" => array( + array('sql_query_pre', ''), + array('sql_query_range', ''), + array('sql_range_step', ''), + array('sql_query', 'SELECT + p.post_id AS id, + p.forum_id, + p.topic_id, + p.poster_id, + IF(p.post_id = t.topic_first_post_id, 1, 0) as topic_first_post, + p.post_time, + p.post_subject, + p.post_subject as title, + p.post_text as data, + t.topic_last_post_time, + 0 as deleted + FROM ' . POSTS_TABLE . ' p, ' . TOPICS_TABLE . ' t + WHERE + p.topic_id = t.topic_id + AND p.post_id >= ( SELECT max_doc_id FROM ' . SPHINX_TABLE . ' WHERE counter_id=1 )'), + ), + "index index_phpbb_{$this->id}_main" => array( + array('path', $config['fulltext_sphinx_data_path'] . "index_phpbb_{$this->id}_main"), + array('source', "source_phpbb_{$this->id}_main"), + array('docinfo', 'extern'), + array('morphology', 'none'), + array('stopwords', (file_exists($config['fulltext_sphinx_config_path'] . 'sphinx_stopwords.txt') && $config['fulltext_sphinx_stopwords']) ? $config['fulltext_sphinx_config_path'] . 'sphinx_stopwords.txt' : ''), + array('min_word_len', '2'), + array('charset_type', 'utf-8'), + array('charset_table', 'U+FF10..U+FF19->0..9, 0..9, U+FF41..U+FF5A->a..z, U+FF21..U+FF3A->a..z, A..Z->a..z, a..z, U+0149, U+017F, U+0138, U+00DF, U+00FF, U+00C0..U+00D6->U+00E0..U+00F6, U+00E0..U+00F6, U+00D8..U+00DE->U+00F8..U+00FE, U+00F8..U+00FE, U+0100->U+0101, U+0101, U+0102->U+0103, U+0103, U+0104->U+0105, U+0105, U+0106->U+0107, U+0107, U+0108->U+0109, U+0109, U+010A->U+010B, U+010B, U+010C->U+010D, U+010D, U+010E->U+010F, U+010F, U+0110->U+0111, U+0111, U+0112->U+0113, U+0113, U+0114->U+0115, U+0115, U+0116->U+0117, U+0117, U+0118->U+0119, U+0119, U+011A->U+011B, U+011B, U+011C->U+011D, U+011D, U+011E->U+011F, U+011F, U+0130->U+0131, U+0131, U+0132->U+0133, U+0133, U+0134->U+0135, U+0135, U+0136->U+0137, U+0137, U+0139->U+013A, U+013A, U+013B->U+013C, U+013C, U+013D->U+013E, U+013E, U+013F->U+0140, U+0140, U+0141->U+0142, U+0142, U+0143->U+0144, U+0144, U+0145->U+0146, U+0146, U+0147->U+0148, U+0148, U+014A->U+014B, U+014B, U+014C->U+014D, U+014D, U+014E->U+014F, U+014F, U+0150->U+0151, U+0151, U+0152->U+0153, U+0153, U+0154->U+0155, U+0155, U+0156->U+0157, U+0157, U+0158->U+0159, U+0159, U+015A->U+015B, U+015B, U+015C->U+015D, U+015D, U+015E->U+015F, U+015F, U+0160->U+0161, U+0161, U+0162->U+0163, U+0163, U+0164->U+0165, U+0165, U+0166->U+0167, U+0167, U+0168->U+0169, U+0169, U+016A->U+016B, U+016B, U+016C->U+016D, U+016D, U+016E->U+016F, U+016F, U+0170->U+0171, U+0171, U+0172->U+0173, U+0173, U+0174->U+0175, U+0175, U+0176->U+0177, U+0177, U+0178->U+00FF, U+00FF, U+0179->U+017A, U+017A, U+017B->U+017C, U+017C, U+017D->U+017E, U+017E, U+ß410..U+042F->U+0430..U+044F, U+0430..U+044F, U+4E00..U+9FFF'), + array('min_prefix_len', '0'), + array('min_infix_len', '0'), + ), + "index index_phpbb_{$this->id}_delta : index_phpbb_{$this->id}_main" => array( + array('path', $config['fulltext_sphinx_data_path'] . "index_phpbb_{$this->id}_delta"), + array('source', "source_phpbb_{$this->id}_delta"), + ), + 'indexer' => array( + array('mem_limit', $config['fulltext_sphinx_indexer_mem_limit'] . 'M'), + ), + 'searchd' => array( + array('address' , '127.0.0.1'), + array('port', ($config['fulltext_sphinx_port']) ? $config['fulltext_sphinx_port'] : '3312'), + array('log', $config['fulltext_sphinx_data_path'] . "log/searchd.log"), + array('query_log', $config['fulltext_sphinx_data_path'] . "log/sphinx-query.log"), + array('read_timeout', '5'), + array('max_children', '30'), + array('pid_file', $config['fulltext_sphinx_data_path'] . "searchd.pid"), + array('max_matches', (string) MAX_MATCHES), + ), + ); + + $non_unique = array('sql_query_pre' => true, 'sql_attr_uint' => true, 'sql_attr_timestamp' => true, 'sql_attr_str2ordinal' => true, 'sql_attr_bool' => true); + $delete = array('sql_group_column' => true, 'sql_date_column' => true, 'sql_str2ordinal_column' => true); + + foreach ($config_data as $section_name => $section_data) + { + $section = &$config_object->get_section_by_name($section_name); + if (!$section) + { + $section = &$config_object->add_section($section_name); + } + + foreach ($delete as $key => $void) + { + $section->delete_variables_by_name($key); + } + + foreach ($non_unique as $key => $void) + { + $section->delete_variables_by_name($key); + } + + foreach ($section_data as $entry) + { + $key = $entry[0]; + $value = $entry[1]; + + if (!isset($non_unique[$key])) + { + $variable = &$section->get_variable_by_name($key); + if (!$variable) + { + $variable = &$section->create_variable($key, $value); + } + else + { + $variable->set_value($value); + } + } + else + { + $variable = &$section->create_variable($key, $value); + } + } + } + + $config_object->write($config['fulltext_sphinx_config_path'] . 'sphinx.conf'); + } + + set_config('fulltext_sphinx_configured', '1'); + + $this->shutdown_searchd(); + $this->tidy(); + + return false; + } + + /** + * Splits keywords entered by a user into an array of words stored in $this->split_words + * Stores the tidied search query in $this->search_query + * + * @param string $keywords Contains the keyword as entered by the user + * @param string $terms is either 'all' or 'any' + * @return false if no valid keywords were found and otherwise true + */ + function split_keywords(&$keywords, $terms) + { + global $config; + + if ($terms == 'all') + { + $match = array('#\sand\s#i', '#\sor\s#i', '#\snot\s#i', '#\+#', '#-#', '#\|#', '#@#'); + $replace = array(' & ', ' | ', ' - ', ' +', ' -', ' |', ''); + + $replacements = 0; + $keywords = preg_replace($match, $replace, $keywords); + $this->sphinx->SetMatchMode(SPH_MATCH_EXTENDED); + } + else + { + $this->sphinx->SetMatchMode(SPH_MATCH_ANY); + } + + $match = array(); + // Keep quotes + $match[] = "#"#"; + // KeepNew lines + $match[] = "#[\n]+#"; + + $replace = array('"', " "); + + $keywords = str_replace(array('"', "\n"), array('"', ' '), trim($keywords)); + + if (strlen($keywords) > 0) + { + $this->search_query = str_replace('"', '"', $keywords); + return true; + } + + return false; + } + + /** + * Performs a search on keywords depending on display specific params. You have to run split_keywords() first. + * + * @param string $type contains either posts or topics depending on what should be searched for + * @param string $fields contains either titleonly (topic titles should be searched), msgonly (only message bodies should be searched), firstpost (only subject and body of the first post should be searched) or all (all post bodies and subjects should be searched) + * @param string $terms is either 'all' (use query as entered, words without prefix should default to "have to be in field") or 'any' (ignore search query parts and just return all posts that contain any of the specified words) + * @param array $sort_by_sql contains SQL code for the ORDER BY part of a query + * @param string $sort_key is the key of $sort_by_sql for the selected sorting + * @param string $sort_dir is either a or d representing ASC and DESC + * @param string $sort_days specifies the maximum amount of days a post may be old + * @param array $ex_fid_ary specifies an array of forum ids which should not be searched + * @param array $m_approve_fid_ary specifies an array of forum ids in which the searcher is allowed to view unapproved posts + * @param int $topic_id is set to 0 or a topic id, if it is not 0 then only posts in this topic should be searched + * @param array $author_ary an array of author ids if the author should be ignored during the search the array is empty + * @param string $author_name specifies the author match, when ANONYMOUS is also a search-match + * @param array &$id_ary passed by reference, to be filled with ids for the page specified by $start and $per_page, should be ordered + * @param int $start indicates the first index of the page + * @param int $per_page number of ids each page is supposed to contain + * @return boolean|int total number of results + * + * @access public + */ + function keyword_search($type, $fields, $terms, $sort_by_sql, $sort_key, $sort_dir, $sort_days, $ex_fid_ary, $m_approve_fid_ary, $topic_id, $author_ary, $author_name, &$id_ary, $start, $per_page) + { + global $config, $db, $auth; + + // No keywords? No posts. + if (!strlen($this->search_query) && !sizeof($author_ary)) + { + return false; + } + + $id_ary = array(); + + $join_topic = ($type == 'posts') ? false : true; + + // sorting + + if ($type == 'topics') + { + switch ($sort_key) + { + case 'a': + $this->sphinx->SetGroupBy('topic_id', SPH_GROUPBY_ATTR, 'poster_id ' . (($sort_dir == 'a') ? 'ASC' : 'DESC')); + break; + case 'f': + $this->sphinx->SetGroupBy('topic_id', SPH_GROUPBY_ATTR, 'forum_id ' . (($sort_dir == 'a') ? 'ASC' : 'DESC')); + break; + case 'i': + case 's': + $this->sphinx->SetGroupBy('topic_id', SPH_GROUPBY_ATTR, 'post_subject ' . (($sort_dir == 'a') ? 'ASC' : 'DESC')); + break; + case 't': + default: + $this->sphinx->SetGroupBy('topic_id', SPH_GROUPBY_ATTR, 'topic_last_post_time ' . (($sort_dir == 'a') ? 'ASC' : 'DESC')); + break; + } + } + else + { + switch ($sort_key) + { + case 'a': + $this->sphinx->SetSortMode(($sort_dir == 'a') ? SPH_SORT_ATTR_ASC : SPH_SORT_ATTR_DESC, 'poster_id'); + break; + case 'f': + $this->sphinx->SetSortMode(($sort_dir == 'a') ? SPH_SORT_ATTR_ASC : SPH_SORT_ATTR_DESC, 'forum_id'); + break; + case 'i': + case 's': + $this->sphinx->SetSortMode(($sort_dir == 'a') ? SPH_SORT_ATTR_ASC : SPH_SORT_ATTR_DESC, 'post_subject'); + break; + case 't': + default: + $this->sphinx->SetSortMode(($sort_dir == 'a') ? SPH_SORT_ATTR_ASC : SPH_SORT_ATTR_DESC, 'post_time'); + break; + } + } + + // most narrow filters first + if ($topic_id) + { + $this->sphinx->SetFilter('topic_id', array($topic_id)); + } + + $search_query_prefix = ''; + + switch($fields) + { + case 'titleonly': + // only search the title + if ($terms == 'all') + { + $search_query_prefix = '@title '; + } + $this->sphinx->SetFieldWeights(array("title" => 5, "data" => 1)); // weight for the title + $this->sphinx->SetFilter('topic_first_post', array(1)); // 1 is first_post, 0 is not first post + break; + + case 'msgonly': + // only search the body + if ($terms == 'all') + { + $search_query_prefix = '@data '; + } + $this->sphinx->SetFieldWeights(array("title" => 1, "data" => 5)); // weight for the body + break; + + case 'firstpost': + $this->sphinx->SetFieldWeights(array("title" => 5, "data" => 1)); // more relative weight for the title, also search the body + $this->sphinx->SetFilter('topic_first_post', array(1)); // 1 is first_post, 0 is not first post + break; + + default: + $this->sphinx->SetFieldWeights(array("title" => 5, "data" => 1)); // more relative weight for the title, also search the body + break; + } + + if (sizeof($author_ary)) + { + $this->sphinx->SetFilter('poster_id', $author_ary); + } + + if (sizeof($ex_fid_ary)) + { + // All forums that a user is allowed to access + $fid_ary = array_unique(array_intersect(array_keys($auth->acl_getf('f_read', true)), array_keys($auth->acl_getf('f_search', true)))); + // All forums that the user wants to and can search in + $search_forums = array_diff($fid_ary, $ex_fid_ary); + + if (sizeof($search_forums)) + { + $this->sphinx->SetFilter('forum_id', $search_forums); + } + } + + $this->sphinx->SetFilter('deleted', array(0)); + + $this->sphinx->SetLimits($start, (int) $per_page, MAX_MATCHES); + $result = $this->sphinx->Query($search_query_prefix . str_replace('"', '"', $this->search_query), $this->indexes); + + // could be connection to localhost:3312 failed (errno=111, msg=Connection refused) during rotate, retry if so + $retries = CONNECT_RETRIES; + while (!$result && (strpos($this->sphinx->_error, "errno=111,") !== false) && $retries--) + { + usleep(CONNECT_WAIT_TIME); + $result = $this->sphinx->Query($search_query_prefix . str_replace('"', '"', $this->search_query), $this->indexes); + } + + $id_ary = array(); + if (isset($result['matches'])) + { + if ($type == 'posts') + { + $id_ary = array_keys($result['matches']); + } + else + { + foreach($result['matches'] as $key => $value) + { + $id_ary[] = $value['attrs']['topic_id']; + } + } + } + else + { + return false; + } + + $result_count = $result['total_found']; + + $id_ary = array_slice($id_ary, 0, (int) $per_page); + + return $result_count; + } + + /** + * Performs a search on an author's posts without caring about message contents. Depends on display specific params + * + * @param string $type contains either posts or topics depending on what should be searched for + * @param boolean $firstpost_only if true, only topic starting posts will be considered + * @param array $sort_by_sql contains SQL code for the ORDER BY part of a query + * @param string $sort_key is the key of $sort_by_sql for the selected sorting + * @param string $sort_dir is either a or d representing ASC and DESC + * @param string $sort_days specifies the maximum amount of days a post may be old + * @param array $ex_fid_ary specifies an array of forum ids which should not be searched + * @param array $m_approve_fid_ary specifies an array of forum ids in which the searcher is allowed to view unapproved posts + * @param int $topic_id is set to 0 or a topic id, if it is not 0 then only posts in this topic should be searched + * @param array $author_ary an array of author ids + * @param string $author_name specifies the author match, when ANONYMOUS is also a search-match + * @param array &$id_ary passed by reference, to be filled with ids for the page specified by $start and $per_page, should be ordered + * @param int $start indicates the first index of the page + * @param int $per_page number of ids each page is supposed to contain + * @return boolean|int total number of results + * + * @access public + */ + function author_search($type, $firstpost_only, $sort_by_sql, $sort_key, $sort_dir, $sort_days, $ex_fid_ary, $m_approve_fid_ary, $topic_id, $author_ary, $author_name, &$id_ary, $start, $per_page) + { + $this->search_query = ''; + + $this->sphinx->SetMatchMode(SPH_MATCH_FULLSCAN); + $fields = ($firstpost_only) ? 'firstpost' : 'all'; + $terms = 'all'; + return $this->keyword_search($type, $fields, $terms, $sort_by_sql, $sort_key, $sort_dir, $sort_days, $ex_fid_ary, $m_approve_fid_ary, $topic_id, $author_ary, $author_name, $id_ary, $start, $per_page); + } + + /** + * Updates wordlist and wordmatch tables when a message is posted or changed + * + * @param string $mode Contains the post mode: edit, post, reply, quote + * @param int $post_id The id of the post which is modified/created + * @param string &$message New or updated post content + * @param string &$subject New or updated post subject + * @param int $poster_id Post author's user id + * @param int $forum_id The id of the forum in which the post is located + * + * @access public + */ + function index($mode, $post_id, &$message, &$subject, $poster_id, $forum_id) + { + global $config, $db; + + if ($mode == 'edit') + { + $this->sphinx->UpdateAttributes($this->indexes, array('forum_id', 'poster_id'), array((int)$post_id => array((int)$forum_id, (int)$poster_id))); + } + else if ($mode != 'post' && $post_id) + { + // update topic_last_post_time for full topic + $sql = 'SELECT p1.post_id + FROM ' . POSTS_TABLE . ' p1 + LEFT JOIN ' . POSTS_TABLE . ' p2 ON (p1.topic_id = p2.topic_id) + WHERE p2.post_id = ' . $post_id; + $result = $db->sql_query($sql); + + $post_updates = array(); + $post_time = time(); + while ($row = $db->sql_fetchrow($result)) + { + $post_updates[(int)$row['post_id']] = array((int) $post_time); + } + $db->sql_freeresult($result); + + if (sizeof($post_updates)) + { + $this->sphinx->UpdateAttributes($this->indexes, array('topic_last_post_time'), $post_updates); + } + } + + if ($config['fulltext_sphinx_autorun']) + { + if ($this->index_created()) + { + $rotate = ($this->searchd_running()) ? ' --rotate' : ''; + + $cwd = getcwd(); + chdir($config['fulltext_sphinx_bin_path']); + exec('./' . INDEXER_NAME . $rotate . ' --config ' . $config['fulltext_sphinx_config_path'] . 'sphinx.conf index_phpbb_' . $this->id . '_delta >> ' . $config['fulltext_sphinx_data_path'] . 'log/indexer.log 2>&1 &'); + var_dump('./' . INDEXER_NAME . $rotate . ' --config ' . $config['fulltext_sphinx_config_path'] . 'sphinx.conf index_phpbb_' . $this->id . '_delta >> ' . $config['fulltext_sphinx_data_path'] . 'log/indexer.log 2>&1 &'); + chdir($cwd); + } + } + } + + /** + * Delete a post from the index after it was deleted + */ + function index_remove($post_ids, $author_ids, $forum_ids) + { + $values = array(); + foreach ($post_ids as $post_id) + { + $values[$post_id] = array(1); + } + + $this->sphinx->UpdateAttributes($this->indexes, array('deleted'), $values); + } + + /** + * Destroy old cache entries + */ + function tidy($create = false) + { + global $config; + + if ($config['fulltext_sphinx_autorun']) + { + if ($this->index_created() || $create) + { + $rotate = ($this->searchd_running()) ? ' --rotate' : ''; + + $cwd = getcwd(); + chdir($config['fulltext_sphinx_bin_path']); + exec('./' . INDEXER_NAME . $rotate . ' --config ' . $config['fulltext_sphinx_config_path'] . 'sphinx.conf index_phpbb_' . $this->id . '_main >> ' . $config['fulltext_sphinx_data_path'] . 'log/indexer.log 2>&1 &'); + exec('./' . INDEXER_NAME . $rotate . ' --config ' . $config['fulltext_sphinx_config_path'] . 'sphinx.conf index_phpbb_' . $this->id . '_delta >> ' . $config['fulltext_sphinx_data_path'] . 'log/indexer.log 2>&1 &'); + chdir($cwd); + } + } + + set_config('search_last_gc', time(), true); + } + + /** + * Create sphinx table + */ + function create_index($acp_module, $u_action) + { + global $db, $user, $config; + + $this->shutdown_searchd(); + + if (!isset($config['fulltext_sphinx_configured']) || !$config['fulltext_sphinx_configured']) + { + $user->add_lang('mods/fulltext_sphinx'); + + return $user->lang['FULLTEXT_SPHINX_CONFIGURE_FIRST']; + } + + if (!$this->index_created()) + { + $sql = 'CREATE TABLE IF NOT EXISTS ' . SPHINX_TABLE . ' ( + counter_id INT NOT NULL PRIMARY KEY, + max_doc_id INT NOT NULL + )'; + $db->sql_query($sql); + + $sql = 'TRUNCATE TABLE ' . SPHINX_TABLE; + $db->sql_query($sql); + } + + // start indexing process + $this->tidy(true); + + $this->shutdown_searchd(); + + return false; + } + + /** + * Drop sphinx table + */ + function delete_index($acp_module, $u_action) + { + global $db, $config; + + $this->shutdown_searchd(); + + if ($config['fulltext_sphinx_autorun']) + { + sphinx_unlink_by_pattern($config['fulltext_sphinx_data_path'], '#^index_phpbb_' . $this->id . '.*$#'); + } + + if (!$this->index_created()) + { + return false; + } + + $sql = 'DROP TABLE ' . SPHINX_TABLE; + $db->sql_query($sql); + + $this->shutdown_searchd(); + + return false; + } + + /** + * Returns true if the sphinx table was created + */ + function index_created($allow_new_files = true) + { + global $db, $config; + + $sql = 'SHOW TABLES LIKE \'' . SPHINX_TABLE . '\''; + $result = $db->sql_query($sql); + $row = $db->sql_fetchrow($result); + $db->sql_freeresult($result); + + $created = false; + + if ($row) + { + if ($config['fulltext_sphinx_autorun']) + { + if ((file_exists($config['fulltext_sphinx_data_path'] . 'index_phpbb_' . $this->id . '_main.spd') && file_exists($config['fulltext_sphinx_data_path'] . 'index_phpbb_' . $this->id . '_delta.spd')) || ($allow_new_files && file_exists($config['fulltext_sphinx_data_path'] . 'index_phpbb_' . $this->id . '_main.new.spd') && file_exists($config['fulltext_sphinx_data_path'] . 'index_phpbb_' . $this->id . '_delta.new.spd'))) + { + $created = true; + } + } + else + { + $created = true; + } + } + + return $created; + } + + /** + * Kills the searchd process and makes sure there's no locks left over + */ + function shutdown_searchd() + { + global $config; + + if ($config['fulltext_sphinx_autorun']) + { + if (!function_exists('exec')) + { + set_config('fulltext_sphinx_autorun', '0'); + return; + } + + exec('killall -9 ' . SEARCHD_NAME . ' >> /dev/null 2>&1 &'); + + if (file_exists($config['fulltext_sphinx_data_path'] . 'searchd.pid')) + { + unlink($config['fulltext_sphinx_data_path'] . 'searchd.pid'); + } + + sphinx_unlink_by_pattern($config['fulltext_sphinx_data_path'], '#^.*\.spl$#'); + } + } + + /** + * Checks whether searchd is running, if it's not running it makes sure there's no left over + * files by calling shutdown_searchd. + * + * @return boolean Whether searchd is running or not + */ + function searchd_running() + { + global $config; + + // if we cannot manipulate the service assume it is running + if (!$config['fulltext_sphinx_autorun']) + { + return true; + } + + if (file_exists($config['fulltext_sphinx_data_path'] . 'searchd.pid')) + { + $pid = trim(file_get_contents($config['fulltext_sphinx_data_path'] . 'searchd.pid')); + + if ($pid) + { + $output = array(); + $pidof_command = 'pidof'; + + exec('whereis -b pidof', $output); + if (sizeof($output) > 1) + { + $output = explode(' ', trim($output[0])); + $pidof_command = $output[1]; // 0 is pidof: + } + + $output = array(); + exec($pidof_command . ' ' . SEARCHD_NAME, $output); + if (sizeof($output) && (trim($output[0]) == $pid || trim($output[1]) == $pid)) + { + return true; + } + } + } + + // make sure it's really not running + $this->shutdown_searchd(); + + return false; + } + + /** + * Returns an associative array containing information about the indexes + */ + function index_stats() + { + global $user; + + if (empty($this->stats)) + { + $this->get_stats(); + } + + $user->add_lang('mods/fulltext_sphinx'); + + return array( + $user->lang['FULLTEXT_SPHINX_MAIN_POSTS'] => ($this->index_created()) ? $this->stats['main_posts'] : 0, + $user->lang['FULLTEXT_SPHINX_DELTA_POSTS'] => ($this->index_created()) ? $this->stats['total_posts'] - $this->stats['main_posts'] : 0, + $user->lang['FULLTEXT_MYSQL_TOTAL_POSTS'] => ($this->index_created()) ? $this->stats['total_posts'] : 0, + $user->lang['FULLTEXT_SPHINX_LAST_SEARCHES'] => nl2br($this->stats['last_searches']), + ); + } + + /** + * Collects stats that can be displayed on the index maintenance page + */ + function get_stats() + { + global $db, $config; + + if ($this->index_created()) + { + $sql = 'SELECT COUNT(post_id) as total_posts + FROM ' . POSTS_TABLE; + $result = $db->sql_query($sql); + $this->stats['total_posts'] = (int) $db->sql_fetchfield('total_posts'); + $db->sql_freeresult($result); + + $sql = 'SELECT COUNT(p.post_id) as main_posts + FROM ' . POSTS_TABLE . ' p, ' . SPHINX_TABLE . ' m + WHERE p.post_id <= m.max_doc_id + AND m.counter_id = 1'; + $result = $db->sql_query($sql); + $this->stats['main_posts'] = (int) $db->sql_fetchfield('main_posts'); + $db->sql_freeresult($result); + } + + $this->stats['last_searches'] = ''; + if ($config['fulltext_sphinx_autorun']) + { + if (file_exists($config['fulltext_sphinx_data_path'] . 'log/sphinx-query.log')) + { + $last_searches = explode("\n", utf8_htmlspecialchars(sphinx_read_last_lines($config['fulltext_sphinx_data_path'] . 'log/sphinx-query.log', 3))); + + foreach($last_searches as $i => $search) + { + if (strpos($search, '[' . $this->indexes . ']') !== false) + { + $last_searches[$i] = str_replace('[' . $this->indexes . ']', '', $search); + } + else + { + $last_searches[$i] = ''; + } + } + $this->stats['last_searches'] = implode("\n", $last_searches); + } + } + } + + /** + * Returns a list of options for the ACP to display + */ + function acp() + { + global $user, $config; + + $user->add_lang('mods/fulltext_sphinx'); + + $config_vars = array( + 'fulltext_sphinx_autoconf' => 'bool', + 'fulltext_sphinx_autorun' => 'bool', + 'fulltext_sphinx_config_path' => 'string', + 'fulltext_sphinx_data_path' => 'string', + 'fulltext_sphinx_bin_path' => 'string', + 'fulltext_sphinx_port' => 'int', + 'fulltext_sphinx_stopwords' => 'bool', + 'fulltext_sphinx_indexer_mem_limit' => 'int', + ); + + $defaults = array( + 'fulltext_sphinx_autoconf' => '1', + 'fulltext_sphinx_autorun' => '1', + 'fulltext_sphinx_indexer_mem_limit' => '512', + ); + + foreach ($config_vars as $config_var => $type) + { + if (!isset($config[$config_var])) + { + $default = ''; + if (isset($defaults[$config_var])) + { + $default = $defaults[$config_var]; + } + set_config($config_var, $default); + } + } + + $no_autoconf = false; + $no_autorun = false; + $bin_path = $config['fulltext_sphinx_bin_path']; + + // try to guess the path if it is empty + if (empty($bin_path)) + { + if (@file_exists('/usr/local/bin/' . INDEXER_NAME) && @file_exists('/usr/local/bin/' . SEARCHD_NAME)) + { + $bin_path = '/usr/local/bin/'; + } + else if (@file_exists('/usr/bin/' . INDEXER_NAME) && @file_exists('/usr/bin/' . SEARCHD_NAME)) + { + $bin_path = '/usr/bin/'; + } + else + { + $output = array(); + if (!function_exists('exec') || null === @exec('whereis -b ' . INDEXER_NAME, $output)) + { + $no_autorun = true; + } + else if (sizeof($output)) + { + $output = explode(' ', $output[0]); + array_shift($output); // remove indexer: + + foreach ($output as $path) + { + $path = dirname($path) . '/'; + + if (file_exists($path . INDEXER_NAME) && file_exists($path . SEARCHD_NAME)) + { + $bin_path = $path; + break; + } + } + } + } + } + + if ($no_autorun) + { + set_config('fulltext_sphinx_autorun', '0'); + } + + if ($no_autoconf) + { + set_config('fulltext_sphinx_autoconf', '0'); + } + + // rewrite config if fulltext sphinx is enabled + if ($config['fulltext_sphinx_autoconf'] && isset($config['fulltext_sphinx_configured']) && $config['fulltext_sphinx_configured']) + { + $this->config_updated(); + } + + // check whether stopwords file is available and enabled + if (@file_exists($config['fulltext_sphinx_config_path'] . 'sphinx_stopwords.txt')) + { + $stopwords_available = true; + $stopwords_active = $config['fulltext_sphinx_stopwords']; + } + else + { + $stopwords_available = false; + $stopwords_active = false; + set_config('fulltext_sphinx_stopwords', '0'); + } + + $tpl = ' + ' . $user->lang['FULLTEXT_SPHINX_CONFIGURE_BEFORE']. ' +
+

' . $user->lang['FULLTEXT_SPHINX_AUTOCONF_EXPLAIN'] . '
+
+
+
+

' . $user->lang['FULLTEXT_SPHINX_AUTORUN_EXPLAIN'] . '
+
+
+
+

' . $user->lang['FULLTEXT_SPHINX_CONFIG_PATH_EXPLAIN'] . '
+
+
+
+

' . $user->lang['FULLTEXT_SPHINX_BIN_PATH_EXPLAIN'] . '
+
+
+
+

' . $user->lang['FULLTEXT_SPHINX_DATA_PATH_EXPLAIN'] . '
+
+
+ ' . $user->lang['FULLTEXT_SPHINX_CONFIGURE_AFTER']. ' +
+

' . $user->lang['FULLTEXT_SPHINX_STOPWORDS_FILE_EXPLAIN'] . '
+
+
+
+

' . $user->lang['FULLTEXT_SPHINX_PORT_EXPLAIN'] . '
+
+
+
+

' . $user->lang['FULLTEXT_SPHINX_INDEXER_MEM_LIMIT_EXPLAIN'] . '
+
' . $user->lang['MIB'] . '
+
+ '; + + // These are fields required in the config table + return array( + 'tpl' => $tpl, + 'config' => $config_vars + ); + } +} + +/** +* Deletes all files from a directory that match a certain pattern +* +* @param string $path Path from which files shall be deleted +* @param string $pattern PCRE pattern that a file needs to match in order to be deleted +*/ +function sphinx_unlink_by_pattern($path, $pattern) +{ + $dir = opendir($path); + while (false !== ($file = readdir($dir))) + { + if (is_file($path . $file) && preg_match($pattern, $file)) + { + unlink($path . $file); + } + } + closedir($dir); +} + +/** +* Reads the last from a file +* +* @param string $file The filename from which the lines shall be read +* @param int $amount The number of lines to be read from the end +* @return string Last lines of the file +*/ +function sphinx_read_last_lines($file, $amount) +{ + $fp = fopen($file, 'r'); + fseek($fp, 0, SEEK_END); + + $c = ''; + $i = 0; + + while ($i < $amount) + { + fseek($fp, -2, SEEK_CUR); + $c = fgetc($fp); + if ($c == "\n") + { + $i++; + } + if (feof($fp)) + { + break; + } + } + + $string = fread($fp, 8192); + fclose($fp); + + return $string; +} + +?> \ No newline at end of file diff --git a/phpBB/includes/sphinxapi-0.9.8.php b/phpBB/includes/sphinxapi-0.9.8.php new file mode 100644 index 0000000000..6a7ea17760 --- /dev/null +++ b/phpBB/includes/sphinxapi-0.9.8.php @@ -0,0 +1,1202 @@ +=8 ) + { + $i = (int)$v; + return pack ( "NN", $i>>32, $i&((1<<32)-1) ); + } + + // x32 route, bcmath + $x = "4294967296"; + if ( function_exists("bcmul") ) + { + $h = bcdiv ( $v, $x, 0 ); + $l = bcmod ( $v, $x ); + return pack ( "NN", (float)$h, (float)$l ); // conversion to float is intentional; int would lose 31st bit + } + + // x32 route, 15 or less decimal digits + // we can use float, because its actually double and has 52 precision bits + if ( strlen($v)<=15 ) + { + $f = (float)$v; + $h = (int)($f/$x); + $l = (int)($f-$x*$h); + return pack ( "NN", $h, $l ); + } + + // x32 route, 16 or more decimal digits + // well, let me know if you *really* need this + die ( "INTERNAL ERROR: packing more than 15-digit numeric on 32-bit PHP is not implemented yet (contact support)" ); +} + + +/// portably unpack 64 unsigned bits, network order to numeric +function sphUnpack64 ( $v ) +{ + list($h,$l) = array_values ( unpack ( "N*N*", $v ) ); + + // x64 route + if ( PHP_INT_SIZE>=8 ) + { + if ( $h<0 ) $h += (1<<32); // because php 5.2.2 to 5.2.5 is totally fucked up again + if ( $l<0 ) $l += (1<<32); + return ($h<<32) + $l; + } + + // x32 route + $h = sprintf ( "%u", $h ); + $l = sprintf ( "%u", $l ); + $x = "4294967296"; + + // bcmath + if ( function_exists("bcmul") ) + return bcadd ( $l, bcmul ( $x, $h ) ); + + // no bcmath, 15 or less decimal digits + // we can use float, because its actually double and has 52 precision bits + if ( $h<1048576 ) + { + $f = ((float)$h)*$x + (float)$l; + return sprintf ( "%.0f", $f ); // builtin conversion is only about 39-40 bits precise! + } + + // x32 route, 16 or more decimal digits + // well, let me know if you *really* need this + die ( "INTERNAL ERROR: unpacking more than 15-digit numeric on 32-bit PHP is not implemented yet (contact support)" ); +} + + +/// sphinx searchd client class +class SphinxClient +{ + var $_host; ///< searchd host (default is "localhost") + var $_port; ///< searchd port (default is 3312) + var $_offset; ///< how many records to seek from result-set start (default is 0) + var $_limit; ///< how many records to return from result-set starting at offset (default is 20) + var $_mode; ///< query matching mode (default is SPH_MATCH_ALL) + var $_weights; ///< per-field weights (default is 1 for all fields) + var $_sort; ///< match sorting mode (default is SPH_SORT_RELEVANCE) + var $_sortby; ///< attribute to sort by (defualt is "") + var $_min_id; ///< min ID to match (default is 0, which means no limit) + var $_max_id; ///< max ID to match (default is 0, which means no limit) + var $_filters; ///< search filters + var $_groupby; ///< group-by attribute name + var $_groupfunc; ///< group-by function (to pre-process group-by attribute value with) + var $_groupsort; ///< group-by sorting clause (to sort groups in result set with) + var $_groupdistinct;///< group-by count-distinct attribute + var $_maxmatches; ///< max matches to retrieve + var $_cutoff; ///< cutoff to stop searching at (default is 0) + var $_retrycount; ///< distributed retries count + var $_retrydelay; ///< distributed retries delay + var $_anchor; ///< geographical anchor point + var $_indexweights; ///< per-index weights + var $_ranker; ///< ranking mode (default is SPH_RANK_PROXIMITY_BM25) + var $_maxquerytime; ///< max query time, milliseconds (default is 0, do not limit) + var $_fieldweights; ///< per-field-name weights + + var $_error; ///< last error message + var $_warning; ///< last warning message + + var $_reqs; ///< requests array for multi-query + var $_mbenc; ///< stored mbstring encoding + var $_arrayresult; ///< whether $result["matches"] should be a hash or an array + var $_timeout; ///< connect timeout + + ///////////////////////////////////////////////////////////////////////////// + // common stuff + ///////////////////////////////////////////////////////////////////////////// + + /// create a new client object and fill defaults + function SphinxClient () + { + // per-client-object settings + $this->_host = "localhost"; + $this->_port = 3312; + + // per-query settings + $this->_offset = 0; + $this->_limit = 20; + $this->_mode = SPH_MATCH_ALL; + $this->_weights = array (); + $this->_sort = SPH_SORT_RELEVANCE; + $this->_sortby = ""; + $this->_min_id = 0; + $this->_max_id = 0; + $this->_filters = array (); + $this->_groupby = ""; + $this->_groupfunc = SPH_GROUPBY_DAY; + $this->_groupsort = "@group desc"; + $this->_groupdistinct= ""; + $this->_maxmatches = 1000; + $this->_cutoff = 0; + $this->_retrycount = 0; + $this->_retrydelay = 0; + $this->_anchor = array (); + $this->_indexweights= array (); + $this->_ranker = SPH_RANK_PROXIMITY_BM25; + $this->_maxquerytime= 0; + $this->_fieldweights= array(); + + $this->_error = ""; // per-reply fields (for single-query case) + $this->_warning = ""; + $this->_reqs = array (); // requests storage (for multi-query case) + $this->_mbenc = ""; + $this->_arrayresult = false; + $this->_timeout = 0; + } + + /// get last error message (string) + function GetLastError () + { + return $this->_error; + } + + /// get last warning message (string) + function GetLastWarning () + { + return $this->_warning; + } + + /// set searchd host name (string) and port (integer) + function SetServer ( $host, $port ) + { + assert ( is_string($host) ); + assert ( is_int($port) ); + $this->_host = $host; + $this->_port = $port; + } + + /// set server connection timeout (0 to remove) + function SetConnectTimeout ( $timeout ) + { + assert ( is_numeric($timeout) ); + $this->_timeout = $timeout; + } + + ///////////////////////////////////////////////////////////////////////////// + + /// enter mbstring workaround mode + function _MBPush () + { + $this->_mbenc = ""; + if ( ini_get ( "mbstring.func_overload" ) & 2 ) + { + $this->_mbenc = mb_internal_encoding(); + mb_internal_encoding ( "latin1" ); + } + } + + /// leave mbstring workaround mode + function _MBPop () + { + if ( $this->_mbenc ) + mb_internal_encoding ( $this->_mbenc ); + } + + /// connect to searchd server + function _Connect ($allow_retry = true) + { + $errno = 0; + $errstr = ""; + if ( $this->_timeout<=0 ) + $fp = @fsockopen ( $this->_host, $this->_port, $errno, $errstr ); + else + $fp = @fsockopen ( $this->_host, $this->_port, $errno, $errstr, $this->_timeout ); + + if ( !$fp ) + { + $errstr = trim ( $errstr ); + $this->_error = "connection to {$this->_host}:{$this->_port} failed (errno=$errno, msg=$errstr)"; + return false; + } + + // check version + //list(,$v) = unpack ( "N*", fread ( $fp, 4 ) ); + $version_data = unpack ( "N*", fread ( $fp, 4 ) ); + if (!isset($version_data[1])) + { + // this should not happen, try to reconnect ONCE + if ($allow_retry) + { + return $this->_Connect(false); + } + else + { + $this->_error = "unexpected version data"; + return false; + } + } + $v = $version_data[1]; + $v = (int)$v; + if ( $v<1 ) + { + fclose ( $fp ); + $this->_error = "expected searchd protocol version 1+, got version '$v'"; + return false; + } + + // all ok, send my version + fwrite ( $fp, pack ( "N", 1 ) ); + return $fp; + } + + /// get and check response packet from searchd server + function _GetResponse ( $fp, $client_ver ) + { + $response = ""; + $len = 0; + + $header = fread ( $fp, 8 ); + if ( strlen($header)==8 ) + { + list ( $status, $ver, $len ) = array_values ( unpack ( "n2a/Nb", $header ) ); + $left = $len; + while ( $left>0 && !feof($fp) ) + { + $chunk = fread ( $fp, $left ); + if ( $chunk ) + { + $response .= $chunk; + $left -= strlen($chunk); + } + } + } + fclose ( $fp ); + + // check response + $read = strlen ( $response ); + if ( !$response || $read!=$len ) + { + $this->_error = $len + ? "failed to read searchd response (status=$status, ver=$ver, len=$len, read=$read)" + : "received zero-sized searchd response"; + return false; + } + + // check status + if ( $status==SEARCHD_WARNING ) + { + list(,$wlen) = unpack ( "N*", substr ( $response, 0, 4 ) ); + $this->_warning = substr ( $response, 4, $wlen ); + return substr ( $response, 4+$wlen ); + } + if ( $status==SEARCHD_ERROR ) + { + $this->_error = "searchd error: " . substr ( $response, 4 ); + return false; + } + if ( $status==SEARCHD_RETRY ) + { + $this->_error = "temporary searchd error: " . substr ( $response, 4 ); + return false; + } + if ( $status!=SEARCHD_OK ) + { + $this->_error = "unknown status code '$status'"; + return false; + } + + // check version + if ( $ver<$client_ver ) + { + $this->_warning = sprintf ( "searchd command v.%d.%d older than client's v.%d.%d, some options might not work", + $ver>>8, $ver&0xff, $client_ver>>8, $client_ver&0xff ); + } + + return $response; + } + + ///////////////////////////////////////////////////////////////////////////// + // searching + ///////////////////////////////////////////////////////////////////////////// + + /// set offset and count into result set, + /// and optionally set max-matches and cutoff limits + function SetLimits ( $offset, $limit, $max=0, $cutoff=0 ) + { + assert ( is_int($offset) ); + assert ( is_int($limit) ); + assert ( $offset>=0 ); + assert ( $limit>0 ); + assert ( $max>=0 ); + $this->_offset = $offset; + $this->_limit = $limit; + if ( $max>0 ) + $this->_maxmatches = $max; + if ( $cutoff>0 ) + $this->_cutoff = $cutoff; + } + + /// set maximum query time, in milliseconds, per-index + /// integer, 0 means "do not limit" + function SetMaxQueryTime ( $max ) + { + assert ( is_int($max) ); + assert ( $max>=0 ); + $this->_maxquerytime = $max; + } + + /// set matching mode + function SetMatchMode ( $mode ) + { + assert ( $mode==SPH_MATCH_ALL + || $mode==SPH_MATCH_ANY + || $mode==SPH_MATCH_PHRASE + || $mode==SPH_MATCH_BOOLEAN + || $mode==SPH_MATCH_EXTENDED + || $mode==SPH_MATCH_FULLSCAN + || $mode==SPH_MATCH_EXTENDED2 ); + $this->_mode = $mode; + } + + /// set ranking mode + function SetRankingMode ( $ranker ) + { + assert ( $ranker==SPH_RANK_PROXIMITY_BM25 + || $ranker==SPH_RANK_BM25 + || $ranker==SPH_RANK_NONE + || $ranker==SPH_RANK_WORDCOUNT ); + $this->_ranker = $ranker; + } + + /// set matches sorting mode + function SetSortMode ( $mode, $sortby="" ) + { + assert ( + $mode==SPH_SORT_RELEVANCE || + $mode==SPH_SORT_ATTR_DESC || + $mode==SPH_SORT_ATTR_ASC || + $mode==SPH_SORT_TIME_SEGMENTS || + $mode==SPH_SORT_EXTENDED || + $mode==SPH_SORT_EXPR ); + assert ( is_string($sortby) ); + assert ( $mode==SPH_SORT_RELEVANCE || strlen($sortby)>0 ); + + $this->_sort = $mode; + $this->_sortby = $sortby; + } + + /// bind per-field weights by order + /// DEPRECATED; use SetFieldWeights() instead + function SetWeights ( $weights ) + { + assert ( is_array($weights) ); + foreach ( $weights as $weight ) + assert ( is_int($weight) ); + + $this->_weights = $weights; + } + + /// bind per-field weights by name + function SetFieldWeights ( $weights ) + { + assert ( is_array($weights) ); + foreach ( $weights as $name=>$weight ) + { + assert ( is_string($name) ); + assert ( is_int($weight) ); + } + $this->_fieldweights = $weights; + } + + /// bind per-index weights by name + function SetIndexWeights ( $weights ) + { + assert ( is_array($weights) ); + foreach ( $weights as $index=>$weight ) + { + assert ( is_string($index) ); + assert ( is_int($weight) ); + } + $this->_indexweights = $weights; + } + + /// set IDs range to match + /// only match records if document ID is beetwen $min and $max (inclusive) + function SetIDRange ( $min, $max ) + { + assert ( is_numeric($min) ); + assert ( is_numeric($max) ); + assert ( $min<=$max ); + $this->_min_id = $min; + $this->_max_id = $max; + } + + /// set values set filter + /// only match records where $attribute value is in given set + function SetFilter ( $attribute, $values, $exclude=false ) + { + assert ( is_string($attribute) ); + assert ( is_array($values) ); + assert ( count($values) ); + + if ( is_array($values) && count($values) ) + { + foreach ( $values as $value ) + assert ( is_numeric($value) ); + + $this->_filters[] = array ( "type"=>SPH_FILTER_VALUES, "attr"=>$attribute, "exclude"=>$exclude, "values"=>$values ); + } + } + + /// set range filter + /// only match records if $attribute value is beetwen $min and $max (inclusive) + function SetFilterRange ( $attribute, $min, $max, $exclude=false ) + { + assert ( is_string($attribute) ); + assert ( is_int($min) ); + assert ( is_int($max) ); + assert ( $min<=$max ); + + $this->_filters[] = array ( "type"=>SPH_FILTER_RANGE, "attr"=>$attribute, "exclude"=>$exclude, "min"=>$min, "max"=>$max ); + } + + /// set float range filter + /// only match records if $attribute value is beetwen $min and $max (inclusive) + function SetFilterFloatRange ( $attribute, $min, $max, $exclude=false ) + { + assert ( is_string($attribute) ); + assert ( is_float($min) ); + assert ( is_float($max) ); + assert ( $min<=$max ); + + $this->_filters[] = array ( "type"=>SPH_FILTER_FLOATRANGE, "attr"=>$attribute, "exclude"=>$exclude, "min"=>$min, "max"=>$max ); + } + + /// setup anchor point for geosphere distance calculations + /// required to use @geodist in filters and sorting + /// latitude and longitude must be in radians + function SetGeoAnchor ( $attrlat, $attrlong, $lat, $long ) + { + assert ( is_string($attrlat) ); + assert ( is_string($attrlong) ); + assert ( is_float($lat) ); + assert ( is_float($long) ); + + $this->_anchor = array ( "attrlat"=>$attrlat, "attrlong"=>$attrlong, "lat"=>$lat, "long"=>$long ); + } + + /// set grouping attribute and function + function SetGroupBy ( $attribute, $func, $groupsort="@group desc" ) + { + assert ( is_string($attribute) ); + assert ( is_string($groupsort) ); + assert ( $func==SPH_GROUPBY_DAY + || $func==SPH_GROUPBY_WEEK + || $func==SPH_GROUPBY_MONTH + || $func==SPH_GROUPBY_YEAR + || $func==SPH_GROUPBY_ATTR + || $func==SPH_GROUPBY_ATTRPAIR ); + + $this->_groupby = $attribute; + $this->_groupfunc = $func; + $this->_groupsort = $groupsort; + } + + /// set count-distinct attribute for group-by queries + function SetGroupDistinct ( $attribute ) + { + assert ( is_string($attribute) ); + $this->_groupdistinct = $attribute; + } + + /// set distributed retries count and delay + function SetRetries ( $count, $delay=0 ) + { + assert ( is_int($count) && $count>=0 ); + assert ( is_int($delay) && $delay>=0 ); + $this->_retrycount = $count; + $this->_retrydelay = $delay; + } + + /// set result set format (hash or array; hash by default) + /// PHP specific; needed for group-by-MVA result sets that may contain duplicate IDs + function SetArrayResult ( $arrayresult ) + { + assert ( is_bool($arrayresult) ); + $this->_arrayresult = $arrayresult; + } + + ////////////////////////////////////////////////////////////////////////////// + + /// clear all filters (for multi-queries) + function ResetFilters () + { + $this->_filters = array(); + $this->_anchor = array(); + } + + /// clear groupby settings (for multi-queries) + function ResetGroupBy () + { + $this->_groupby = ""; + $this->_groupfunc = SPH_GROUPBY_DAY; + $this->_groupsort = "@group desc"; + $this->_groupdistinct= ""; + } + + ////////////////////////////////////////////////////////////////////////////// + + /// connect to searchd server, run given search query through given indexes, + /// and return the search results + function Query ( $query, $index="*", $comment="" ) + { + assert ( empty($this->_reqs) ); + + $this->AddQuery ( $query, $index, $comment ); + $results = $this->RunQueries (); + $this->_reqs = array (); // just in case it failed too early + + if ( !is_array($results) ) + return false; // probably network error; error message should be already filled + + $this->_error = $results[0]["error"]; + $this->_warning = $results[0]["warning"]; + if ( $results[0]["status"]==SEARCHD_ERROR ) + return false; + else + return $results[0]; + } + + /// helper to pack floats in network byte order + function _PackFloat ( $f ) + { + $t1 = pack ( "f", $f ); // machine order + list(,$t2) = unpack ( "L*", $t1 ); // int in machine order + return pack ( "N", $t2 ); + } + + /// add query to multi-query batch + /// returns index into results array from RunQueries() call + function AddQuery ( $query, $index="*", $comment="" ) + { + // mbstring workaround + $this->_MBPush (); + + // build request + $req = pack ( "NNNNN", $this->_offset, $this->_limit, $this->_mode, $this->_ranker, $this->_sort ); // mode and limits + $req .= pack ( "N", strlen($this->_sortby) ) . $this->_sortby; + $req .= pack ( "N", strlen($query) ) . $query; // query itself + $req .= pack ( "N", count($this->_weights) ); // weights + foreach ( $this->_weights as $weight ) + $req .= pack ( "N", (int)$weight ); + $req .= pack ( "N", strlen($index) ) . $index; // indexes + $req .= pack ( "N", 1 ); // id64 range marker + $req .= sphPack64 ( $this->_min_id ) . sphPack64 ( $this->_max_id ); // id64 range + + // filters + $req .= pack ( "N", count($this->_filters) ); + foreach ( $this->_filters as $filter ) + { + $req .= pack ( "N", strlen($filter["attr"]) ) . $filter["attr"]; + $req .= pack ( "N", $filter["type"] ); + switch ( $filter["type"] ) + { + case SPH_FILTER_VALUES: + $req .= pack ( "N", count($filter["values"]) ); + foreach ( $filter["values"] as $value ) + $req .= pack ( "N", floatval($value) ); // this uberhack is to workaround 32bit signed int limit on x32 platforms + break; + + case SPH_FILTER_RANGE: + $req .= pack ( "NN", $filter["min"], $filter["max"] ); + break; + + case SPH_FILTER_FLOATRANGE: + $req .= $this->_PackFloat ( $filter["min"] ) . $this->_PackFloat ( $filter["max"] ); + break; + + default: + assert ( 0 && "internal error: unhandled filter type" ); + } + $req .= pack ( "N", $filter["exclude"] ); + } + + // group-by clause, max-matches count, group-sort clause, cutoff count + $req .= pack ( "NN", $this->_groupfunc, strlen($this->_groupby) ) . $this->_groupby; + $req .= pack ( "N", $this->_maxmatches ); + $req .= pack ( "N", strlen($this->_groupsort) ) . $this->_groupsort; + $req .= pack ( "NNN", $this->_cutoff, $this->_retrycount, $this->_retrydelay ); + $req .= pack ( "N", strlen($this->_groupdistinct) ) . $this->_groupdistinct; + + // anchor point + if ( empty($this->_anchor) ) + { + $req .= pack ( "N", 0 ); + } else + { + $a =& $this->_anchor; + $req .= pack ( "N", 1 ); + $req .= pack ( "N", strlen($a["attrlat"]) ) . $a["attrlat"]; + $req .= pack ( "N", strlen($a["attrlong"]) ) . $a["attrlong"]; + $req .= $this->_PackFloat ( $a["lat"] ) . $this->_PackFloat ( $a["long"] ); + } + + // per-index weights + $req .= pack ( "N", count($this->_indexweights) ); + foreach ( $this->_indexweights as $idx=>$weight ) + $req .= pack ( "N", strlen($idx) ) . $idx . pack ( "N", $weight ); + + // max query time + $req .= pack ( "N", $this->_maxquerytime ); + + // per-field weights + $req .= pack ( "N", count($this->_fieldweights) ); + foreach ( $this->_fieldweights as $field=>$weight ) + $req .= pack ( "N", strlen($field) ) . $field . pack ( "N", $weight ); + + // comment + $req .= pack ( "N", strlen($comment) ) . $comment; + + // mbstring workaround + $this->_MBPop (); + + // store request to requests array + $this->_reqs[] = $req; + return count($this->_reqs)-1; + } + + /// connect to searchd, run queries batch, and return an array of result sets + function RunQueries () + { + if ( empty($this->_reqs) ) + { + $this->_error = "no queries defined, issue AddQuery() first"; + return false; + } + + // mbstring workaround + $this->_MBPush (); + + if (!( $fp = $this->_Connect() )) + { + $this->_MBPop (); + return false; + } + + //////////////////////////// + // send query, get response + //////////////////////////// + + $nreqs = count($this->_reqs); + $req = join ( "", $this->_reqs ); + $len = 4+strlen($req); + $req = pack ( "nnNN", SEARCHD_COMMAND_SEARCH, VER_COMMAND_SEARCH, $len, $nreqs ) . $req; // add header + + fwrite ( $fp, $req, $len+8 ); + if (!( $response = $this->_GetResponse ( $fp, VER_COMMAND_SEARCH ) )) + { + $this->_MBPop (); + return false; + } + + $this->_reqs = array (); + + ////////////////// + // parse response + ////////////////// + + $p = 0; // current position + $max = strlen($response); // max position for checks, to protect against broken responses + + $results = array (); + for ( $ires=0; $ires<$nreqs && $p<$max; $ires++ ) + { + $results[] = array(); + $result =& $results[$ires]; + + $result["error"] = ""; + $result["warning"] = ""; + + // extract status + list(,$status) = unpack ( "N*", substr ( $response, $p, 4 ) ); $p += 4; + $result["status"] = $status; + if ( $status!=SEARCHD_OK ) + { + list(,$len) = unpack ( "N*", substr ( $response, $p, 4 ) ); $p += 4; + $message = substr ( $response, $p, $len ); $p += $len; + + if ( $status==SEARCHD_WARNING ) + { + $result["warning"] = $message; + } else + { + $result["error"] = $message; + continue; + } + } + + // read schema + $fields = array (); + $attrs = array (); + + list(,$nfields) = unpack ( "N*", substr ( $response, $p, 4 ) ); $p += 4; + while ( $nfields-->0 && $p<$max ) + { + list(,$len) = unpack ( "N*", substr ( $response, $p, 4 ) ); $p += 4; + $fields[] = substr ( $response, $p, $len ); $p += $len; + } + $result["fields"] = $fields; + + list(,$nattrs) = unpack ( "N*", substr ( $response, $p, 4 ) ); $p += 4; + while ( $nattrs-->0 && $p<$max ) + { + list(,$len) = unpack ( "N*", substr ( $response, $p, 4 ) ); $p += 4; + $attr = substr ( $response, $p, $len ); $p += $len; + list(,$type) = unpack ( "N*", substr ( $response, $p, 4 ) ); $p += 4; + $attrs[$attr] = $type; + } + $result["attrs"] = $attrs; + + // read match count + list(,$count) = unpack ( "N*", substr ( $response, $p, 4 ) ); $p += 4; + list(,$id64) = unpack ( "N*", substr ( $response, $p, 4 ) ); $p += 4; + + // read matches + $idx = -1; + while ( $count-->0 && $p<$max ) + { + // index into result array + $idx++; + + // parse document id and weight + if ( $id64 ) + { + $doc = sphUnpack64 ( substr ( $response, $p, 8 ) ); $p += 8; + list(,$weight) = unpack ( "N*", substr ( $response, $p, 4 ) ); $p += 4; + } else + { + list ( $doc, $weight ) = array_values ( unpack ( "N*N*", + substr ( $response, $p, 8 ) ) ); + $p += 8; + + if ( PHP_INT_SIZE>=8 ) + { + // x64 route, workaround broken unpack() in 5.2.2+ + if ( $doc<0 ) $doc += (1<<32); + } else + { + // x32 route, workaround php signed/unsigned braindamage + $doc = sprintf ( "%u", $doc ); + } + } + $weight = sprintf ( "%u", $weight ); + + // create match entry + if ( $this->_arrayresult ) + $result["matches"][$idx] = array ( "id"=>$doc, "weight"=>$weight ); + else + $result["matches"][$doc]["weight"] = $weight; + + // parse and create attributes + $attrvals = array (); + foreach ( $attrs as $attr=>$type ) + { + // handle floats + if ( $type==SPH_ATTR_FLOAT ) + { + list(,$uval) = unpack ( "N*", substr ( $response, $p, 4 ) ); $p += 4; + list(,$fval) = unpack ( "f*", pack ( "L", $uval ) ); + $attrvals[$attr] = $fval; + continue; + } + + // handle everything else as unsigned ints + list(,$val) = unpack ( "N*", substr ( $response, $p, 4 ) ); $p += 4; + if ( $type & SPH_ATTR_MULTI ) + { + $attrvals[$attr] = array (); + $nvalues = $val; + while ( $nvalues-->0 && $p<$max ) + { + list(,$val) = unpack ( "N*", substr ( $response, $p, 4 ) ); $p += 4; + $attrvals[$attr][] = sprintf ( "%u", $val ); + } + } else + { + $attrvals[$attr] = sprintf ( "%u", $val ); + } + } + + if ( $this->_arrayresult ) + $result["matches"][$idx]["attrs"] = $attrvals; + else + $result["matches"][$doc]["attrs"] = $attrvals; + } + + list ( $total, $total_found, $msecs, $words ) = + array_values ( unpack ( "N*N*N*N*", substr ( $response, $p, 16 ) ) ); + $result["total"] = sprintf ( "%u", $total ); + $result["total_found"] = sprintf ( "%u", $total_found ); + $result["time"] = sprintf ( "%.3f", $msecs/1000 ); + $p += 16; + + while ( $words-->0 && $p<$max ) + { + list(,$len) = unpack ( "N*", substr ( $response, $p, 4 ) ); $p += 4; + $word = substr ( $response, $p, $len ); $p += $len; + list ( $docs, $hits ) = array_values ( unpack ( "N*N*", substr ( $response, $p, 8 ) ) ); $p += 8; + $result["words"][$word] = array ( + "docs"=>sprintf ( "%u", $docs ), + "hits"=>sprintf ( "%u", $hits ) ); + } + } + + $this->_MBPop (); + return $results; + } + + ///////////////////////////////////////////////////////////////////////////// + // excerpts generation + ///////////////////////////////////////////////////////////////////////////// + + /// connect to searchd server, and generate exceprts (snippets) + /// of given documents for given query. returns false on failure, + /// an array of snippets on success + function BuildExcerpts ( $docs, $index, $words, $opts=array() ) + { + assert ( is_array($docs) ); + assert ( is_string($index) ); + assert ( is_string($words) ); + assert ( is_array($opts) ); + + $this->_MBPush (); + + if (!( $fp = $this->_Connect() )) + { + $this->_MBPop(); + return false; + } + + ///////////////// + // fixup options + ///////////////// + + if ( !isset($opts["before_match"]) ) $opts["before_match"] = ""; + if ( !isset($opts["after_match"]) ) $opts["after_match"] = ""; + if ( !isset($opts["chunk_separator"]) ) $opts["chunk_separator"] = " ... "; + if ( !isset($opts["limit"]) ) $opts["limit"] = 256; + if ( !isset($opts["around"]) ) $opts["around"] = 5; + if ( !isset($opts["exact_phrase"]) ) $opts["exact_phrase"] = false; + if ( !isset($opts["single_passage"]) ) $opts["single_passage"] = false; + if ( !isset($opts["use_boundaries"]) ) $opts["use_boundaries"] = false; + if ( !isset($opts["weight_order"]) ) $opts["weight_order"] = false; + + ///////////////// + // build request + ///////////////// + + // v.1.0 req + $flags = 1; // remove spaces + if ( $opts["exact_phrase"] ) $flags |= 2; + if ( $opts["single_passage"] ) $flags |= 4; + if ( $opts["use_boundaries"] ) $flags |= 8; + if ( $opts["weight_order"] ) $flags |= 16; + $req = pack ( "NN", 0, $flags ); // mode=0, flags=$flags + $req .= pack ( "N", strlen($index) ) . $index; // req index + $req .= pack ( "N", strlen($words) ) . $words; // req words + + // options + $req .= pack ( "N", strlen($opts["before_match"]) ) . $opts["before_match"]; + $req .= pack ( "N", strlen($opts["after_match"]) ) . $opts["after_match"]; + $req .= pack ( "N", strlen($opts["chunk_separator"]) ) . $opts["chunk_separator"]; + $req .= pack ( "N", (int)$opts["limit"] ); + $req .= pack ( "N", (int)$opts["around"] ); + + // documents + $req .= pack ( "N", count($docs) ); + foreach ( $docs as $doc ) + { + assert ( is_string($doc) ); + $req .= pack ( "N", strlen($doc) ) . $doc; + } + + //////////////////////////// + // send query, get response + //////////////////////////// + + $len = strlen($req); + $req = pack ( "nnN", SEARCHD_COMMAND_EXCERPT, VER_COMMAND_EXCERPT, $len ) . $req; // add header + $wrote = fwrite ( $fp, $req, $len+8 ); + if (!( $response = $this->_GetResponse ( $fp, VER_COMMAND_EXCERPT ) )) + { + $this->_MBPop (); + return false; + } + + ////////////////// + // parse response + ////////////////// + + $pos = 0; + $res = array (); + $rlen = strlen($response); + for ( $i=0; $i $rlen ) + { + $this->_error = "incomplete reply"; + $this->_MBPop (); + return false; + } + $res[] = $len ? substr ( $response, $pos, $len ) : ""; + $pos += $len; + } + + $this->_MBPop (); + return $res; + } + + + ///////////////////////////////////////////////////////////////////////////// + // keyword generation + ///////////////////////////////////////////////////////////////////////////// + + /// connect to searchd server, and generate keyword list for a given query + /// returns false on failure, + /// an array of words on success + function BuildKeywords ( $query, $index, $hits ) + { + assert ( is_string($query) ); + assert ( is_string($index) ); + assert ( is_bool($hits) ); + + $this->_MBPush (); + + if (!( $fp = $this->_Connect() )) + { + $this->_MBPop(); + return false; + } + + ///////////////// + // build request + ///////////////// + + // v.1.0 req + $req = pack ( "N", strlen($query) ) . $query; // req query + $req .= pack ( "N", strlen($index) ) . $index; // req index + $req .= pack ( "N", (int)$hits ); + + //////////////////////////// + // send query, get response + //////////////////////////// + + $len = strlen($req); + $req = pack ( "nnN", SEARCHD_COMMAND_KEYWORDS, VER_COMMAND_KEYWORDS, $len ) . $req; // add header + $wrote = fwrite ( $fp, $req, $len+8 ); + if (!( $response = $this->_GetResponse ( $fp, VER_COMMAND_KEYWORDS ) )) + { + $this->_MBPop (); + return false; + } + + ////////////////// + // parse response + ////////////////// + + $pos = 0; + $res = array (); + $rlen = strlen($response); + list(,$nwords) = unpack ( "N*", substr ( $response, $pos, 4 ) ); + $pos += 4; + for ( $i=0; $i<$nwords; $i++ ) + { + list(,$len) = unpack ( "N*", substr ( $response, $pos, 4 ) ); $pos += 4; + $tokenized = $len ? substr ( $response, $pos, $len ) : ""; + $pos += $len; + + list(,$len) = unpack ( "N*", substr ( $response, $pos, 4 ) ); $pos += 4; + $normalized = $len ? substr ( $response, $pos, $len ) : ""; + $pos += $len; + + $res[] = array ( "tokenized"=>$tokenized, "normalized"=>$normalized ); + + if ( $hits ) + { + list($ndocs,$nhits) = array_values ( unpack ( "N*N*", substr ( $response, $pos, 8 ) ) ); + $pos += 8; + $res [$i]["docs"] = $ndocs; + $res [$i]["hits"] = $nhits; + } + + if ( $pos > $rlen ) + { + $this->_error = "incomplete reply"; + $this->_MBPop (); + return false; + } + } + + $this->_MBPop (); + return $res; + } + + function EscapeString ( $string ) + { + $from = array ( '(',')','|','-','!','@','~','"','&', '/' ); + $to = array ( '\(','\)','\|','\-','\!','\@','\~','\"', '\&', '\/' ); + + return str_replace ( $from, $to, $string ); + } + + ///////////////////////////////////////////////////////////////////////////// + // attribute updates + ///////////////////////////////////////////////////////////////////////////// + + /// update given attribute values on given documents in given indexes + /// returns amount of updated documents (0 or more) on success, or -1 on failure + function UpdateAttributes ( $index, $attrs, $values ) + { + // verify everything + assert ( is_string($index) ); + + assert ( is_array($attrs) ); + foreach ( $attrs as $attr ) + assert ( is_string($attr) ); + + assert ( is_array($values) ); + foreach ( $values as $id=>$entry ) + { + assert ( is_numeric($id) ); + assert ( is_array($entry) ); + assert ( count($entry)==count($attrs) ); + foreach ( $entry as $v ) + assert ( is_int($v) ); + } + + // build request + $req = pack ( "N", strlen($index) ) . $index; + + $req .= pack ( "N", count($attrs) ); + foreach ( $attrs as $attr ) + $req .= pack ( "N", strlen($attr) ) . $attr; + + $req .= pack ( "N", count($values) ); + foreach ( $values as $id=>$entry ) + { + $req .= sphPack64 ( $id ); + foreach ( $entry as $v ) + $req .= pack ( "N", $v ); + } + + // mbstring workaround + $this->_MBPush (); + + // connect, send query, get response + if (!( $fp = $this->_Connect() )) + { + $this->_MBPop (); + return -1; + } + + $len = strlen($req); + $req = pack ( "nnN", SEARCHD_COMMAND_UPDATE, VER_COMMAND_UPDATE, $len ) . $req; // add header + fwrite ( $fp, $req, $len+8 ); + + if (!( $response = $this->_GetResponse ( $fp, VER_COMMAND_UPDATE ) )) + { + $this->_MBPop (); + return -1; + } + + // parse response + list(,$updated) = unpack ( "N*", substr ( $response, 0, 4 ) ); + $this->_MBPop (); + return $updated; + } +} + +// +// $Id$ +// \ No newline at end of file diff --git a/phpBB/language/en/mods/fulltext_sphinx.php b/phpBB/language/en/mods/fulltext_sphinx.php new file mode 100644 index 0000000000..e06328afc8 --- /dev/null +++ b/phpBB/language/en/mods/fulltext_sphinx.php @@ -0,0 +1,65 @@ + 'Automatically configure Sphinx', + 'FULLTEXT_SPHINX_AUTOCONF_EXPLAIN' => 'This is the easiest way to install Sphinx, just select the settings here and a config file will be written for you. This requires write permissions on the configuration folder.', + 'FULLTEXT_SPHINX_AUTORUN' => 'Automatically run Sphinx', + 'FULLTEXT_SPHINX_AUTORUN_EXPLAIN' => 'This is the easiest way to run Sphinx. Select the paths in this dialogue and the Sphinx daemon will be started and stopped as needed. You can also create an index from the ACP. If your PHP installation forbids the use of exec you can disable this and run Sphinx manually.', + 'FULLTEXT_SPHINX_BIN_PATH' => 'Path to executables directory', + 'FULLTEXT_SPHINX_BIN_PATH_EXPLAIN' => 'Skip if autorun is disabled. If this path could not be determined automatically you have to enter the path to the directory in which the sphinx executables indexer and searchd reside.', + 'FULLTEXT_SPHINX_CONFIG_PATH' => 'Path to configuration directory', + 'FULLTEXT_SPHINX_CONFIG_PATH_EXPLAIN' => 'Skip if autoconf is disabled. You should create this config directory outside the web accessable directories. It has to be writable by the user as which your webserver is running (often www-data or nobody).', + 'FULLTEXT_SPHINX_CONFIGURE_FIRST' => 'Before you create an index you have to enable and configure sphinx under GENERAL -> SERVER CONFIGURATION -> Search settings.', + 'FULLTEXT_SPHINX_CONFIGURE_BEFORE' => 'Configure the following settings BEFORE activating Sphinx', + 'FULLTEXT_SPHINX_CONFIGURE_AFTER' => 'The following settings do not have to be configured before activating Sphinx', + 'FULLTEXT_SPHINX_DATA_PATH' => 'Path to data directory', + 'FULLTEXT_SPHINX_DATA_PATH_EXPLAIN' => 'Skip if autorun is disabled. You should create this directory outside the web accessable directories. It has to be writable by the user as which your webserver is running (often www-data or nobody). It will be used to store the indexes and log files.', + 'FULLTEXT_SPHINX_DELTA_POSTS' => 'Number of posts in frequently updated delta index', + 'FULLTEXT_SPHINX_DIRECTORY_NOT_FOUND' => 'The directory %s does not exist. Please correct your path settings.', + 'FULLTEXT_SPHINX_FILE_NOT_EXECUTABLE' => 'The file %s is not executable for the webserver.', + 'FULLTEXT_SPHINX_FILE_NOT_FOUND' => 'The file %s does not exist. Please correct your path settings.', + 'FULLTEXT_SPHINX_FILE_NOT_WRITABLE' => 'The file %s cannot be written by the webserver.', + 'FULLTEXT_SPHINX_INDEXER_MEM_LIMIT' => 'Indexer memory limit', + 'FULLTEXT_SPHINX_INDEXER_MEM_LIMIT_EXPLAIN' => 'This number should at all times be lower than the RAM available on your machine. If you experience periodic performance problems this might be due to the indexer consuming too many resources. It might help to lower the amount of memory available to the indexer.', + 'FULLTEXT_SPHINX_LAST_SEARCHES' => 'Recent search queries', + 'FULLTEXT_SPHINX_MAIN_POSTS' => 'Number of posts in main index', + 'FULLTEXT_SPHINX_PORT' => 'Sphinx search deamon port', + 'FULLTEXT_SPHINX_PORT_EXPLAIN' => 'Port on which the sphinx search deamon on localhost listens. Leave empty to use the default 3312', + 'FULLTEXT_SPHINX_REQUIRES_EXEC' => 'The sphinx plugin for phpBB requires PHP’s exec function which is disabled on your system.', + 'FULLTEXT_SPHINX_UNCONFIGURED' => 'Please set all necessary options in the "Fulltext Sphinx" section of the previous page before you try to activate the sphinx plugin.', + 'FULLTEXT_SPHINX_WRONG_DATABASE' => 'The sphinx plugin for phpBB currently only supports MySQL', + 'FULLTEXT_SPHINX_STOPWORDS_FILE' => 'Stopwords activated', + 'FULLTEXT_SPHINX_STOPWORDS_FILE_EXPLAIN' => 'This setting only works with autoconf enabled. You can place a file called sphinx_stopwords.txt containing one word in each line in your config directory. If this file is present these words will be excluded from the indexing process.', +)); + +?> \ No newline at end of file -- cgit v1.2.1 From fcf0d04b20f1c862117a8ab962d692bd2b8b074f Mon Sep 17 00:00:00 2001 From: Dhruv Goel Date: Wed, 9 May 2012 19:13:36 +0530 Subject: [feature/sphinx-fulltext-search] minor changes some minor code changes to make it working against current develop and comply with other search backend coding convetions. PHPBB3-10946 --- phpBB/includes/search/fulltext_sphinx.php | 32 +++++++++++++++++-------------- 1 file changed, 18 insertions(+), 14 deletions(-) (limited to 'phpBB') diff --git a/phpBB/includes/search/fulltext_sphinx.php b/phpBB/includes/search/fulltext_sphinx.php index e0c467df93..ef357970a0 100644 --- a/phpBB/includes/search/fulltext_sphinx.php +++ b/phpBB/includes/search/fulltext_sphinx.php @@ -18,33 +18,27 @@ if (!defined('IN_PHPBB')) /** * @ignore */ +/** +* This statement is necessary as this file is sometimes included from within a +* function and the variables used are in global space. +*/ +global $phpbb_root_path, $phpEx, $table_prefix; require($phpbb_root_path . "includes/sphinxapi-0.9.8." . $phpEx); define('INDEXER_NAME', 'indexer'); define('SEARCHD_NAME', 'searchd'); -define('SPHINX_TABLE', table_prefix() . 'sphinx'); +define('SPHINX_TABLE', $table_prefix . 'sphinx'); define('MAX_MATCHES', 20000); define('CONNECT_RETRIES', 3); define('CONNECT_WAIT_TIME', 300); -/** -* Returns the global table prefix -* This function is necessary as this file is sometimes included from within a -* function and table_prefix is in global space. -*/ -function table_prefix() -{ - global $table_prefix; - return $table_prefix; -} - /** * fulltext_sphinx * Fulltext search based on the sphinx search deamon * @package search */ -class fulltext_sphinx +class phpbb_search_fulltext_sphinx { var $stats = array(); var $word_length = array(); @@ -53,7 +47,7 @@ class fulltext_sphinx var $common_words = array(); var $id; - function fulltext_sphinx(&$error) + public function __construct(&$error) { global $config; @@ -82,6 +76,16 @@ class fulltext_sphinx $error = false; } + + /** + * Returns the name of this search backend to be displayed to administrators + * + * @return string Name + */ + public function get_name() + { + return 'Sphinx Fulltext'; + } /** * Checks permissions and paths, if everything is correct it generates the config file -- cgit v1.2.1 From 99d4660df68d71ea56cccb150ae858c1dd7575b8 Mon Sep 17 00:00:00 2001 From: Dhruv Goel Date: Wed, 20 Jun 2012 05:11:53 +0530 Subject: [feature/sphinx-fulltext-search] update config file Sphinx config file updated according to new documentation. PHPBB3-10946 --- phpBB/includes/search/fulltext_sphinx.php | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) (limited to 'phpBB') diff --git a/phpBB/includes/search/fulltext_sphinx.php b/phpBB/includes/search/fulltext_sphinx.php index ef357970a0..6c5092f4aa 100644 --- a/phpBB/includes/search/fulltext_sphinx.php +++ b/phpBB/includes/search/fulltext_sphinx.php @@ -295,7 +295,7 @@ class phpbb_search_fulltext_sphinx array('stopwords', (file_exists($config['fulltext_sphinx_config_path'] . 'sphinx_stopwords.txt') && $config['fulltext_sphinx_stopwords']) ? $config['fulltext_sphinx_config_path'] . 'sphinx_stopwords.txt' : ''), array('min_word_len', '2'), array('charset_type', 'utf-8'), - array('charset_table', 'U+FF10..U+FF19->0..9, 0..9, U+FF41..U+FF5A->a..z, U+FF21..U+FF3A->a..z, A..Z->a..z, a..z, U+0149, U+017F, U+0138, U+00DF, U+00FF, U+00C0..U+00D6->U+00E0..U+00F6, U+00E0..U+00F6, U+00D8..U+00DE->U+00F8..U+00FE, U+00F8..U+00FE, U+0100->U+0101, U+0101, U+0102->U+0103, U+0103, U+0104->U+0105, U+0105, U+0106->U+0107, U+0107, U+0108->U+0109, U+0109, U+010A->U+010B, U+010B, U+010C->U+010D, U+010D, U+010E->U+010F, U+010F, U+0110->U+0111, U+0111, U+0112->U+0113, U+0113, U+0114->U+0115, U+0115, U+0116->U+0117, U+0117, U+0118->U+0119, U+0119, U+011A->U+011B, U+011B, U+011C->U+011D, U+011D, U+011E->U+011F, U+011F, U+0130->U+0131, U+0131, U+0132->U+0133, U+0133, U+0134->U+0135, U+0135, U+0136->U+0137, U+0137, U+0139->U+013A, U+013A, U+013B->U+013C, U+013C, U+013D->U+013E, U+013E, U+013F->U+0140, U+0140, U+0141->U+0142, U+0142, U+0143->U+0144, U+0144, U+0145->U+0146, U+0146, U+0147->U+0148, U+0148, U+014A->U+014B, U+014B, U+014C->U+014D, U+014D, U+014E->U+014F, U+014F, U+0150->U+0151, U+0151, U+0152->U+0153, U+0153, U+0154->U+0155, U+0155, U+0156->U+0157, U+0157, U+0158->U+0159, U+0159, U+015A->U+015B, U+015B, U+015C->U+015D, U+015D, U+015E->U+015F, U+015F, U+0160->U+0161, U+0161, U+0162->U+0163, U+0163, U+0164->U+0165, U+0165, U+0166->U+0167, U+0167, U+0168->U+0169, U+0169, U+016A->U+016B, U+016B, U+016C->U+016D, U+016D, U+016E->U+016F, U+016F, U+0170->U+0171, U+0171, U+0172->U+0173, U+0173, U+0174->U+0175, U+0175, U+0176->U+0177, U+0177, U+0178->U+00FF, U+00FF, U+0179->U+017A, U+017A, U+017B->U+017C, U+017C, U+017D->U+017E, U+017E, U+ß410..U+042F->U+0430..U+044F, U+0430..U+044F, U+4E00..U+9FFF'), + array('charset_table', 'U+FF10..U+FF19->0..9, 0..9, U+FF41..U+FF5A->a..z, U+FF21..U+FF3A->a..z, A..Z->a..z, a..z, U+0149, U+017F, U+0138, U+00DF, U+00FF, U+00C0..U+00D6->U+00E0..U+00F6, U+00E0..U+00F6, U+00D8..U+00DE->U+00F8..U+00FE, U+00F8..U+00FE, U+0100->U+0101, U+0101, U+0102->U+0103, U+0103, U+0104->U+0105, U+0105, U+0106->U+0107, U+0107, U+0108->U+0109, U+0109, U+010A->U+010B, U+010B, U+010C->U+010D, U+010D, U+010E->U+010F, U+010F, U+0110->U+0111, U+0111, U+0112->U+0113, U+0113, U+0114->U+0115, U+0115, U+0116->U+0117, U+0117, U+0118->U+0119, U+0119, U+011A->U+011B, U+011B, U+011C->U+011D, U+011D, U+011E->U+011F, U+011F, U+0130->U+0131, U+0131, U+0132->U+0133, U+0133, U+0134->U+0135, U+0135, U+0136->U+0137, U+0137, U+0139->U+013A, U+013A, U+013B->U+013C, U+013C, U+013D->U+013E, U+013E, U+013F->U+0140, U+0140, U+0141->U+0142, U+0142, U+0143->U+0144, U+0144, U+0145->U+0146, U+0146, U+0147->U+0148, U+0148, U+014A->U+014B, U+014B, U+014C->U+014D, U+014D, U+014E->U+014F, U+014F, U+0150->U+0151, U+0151, U+0152->U+0153, U+0153, U+0154->U+0155, U+0155, U+0156->U+0157, U+0157, U+0158->U+0159, U+0159, U+015A->U+015B, U+015B, U+015C->U+015D, U+015D, U+015E->U+015F, U+015F, U+0160->U+0161, U+0161, U+0162->U+0163, U+0163, U+0164->U+0165, U+0165, U+0166->U+0167, U+0167, U+0168->U+0169, U+0169, U+016A->U+016B, U+016B, U+016C->U+016D, U+016D, U+016E->U+016F, U+016F, U+0170->U+0171, U+0171, U+0172->U+0173, U+0173, U+0174->U+0175, U+0175, U+0176->U+0177, U+0177, U+0178->U+00FF, U+00FF, U+0179->U+017A, U+017A, U+017B->U+017C, U+017C, U+017D->U+017E, U+017E, U+0410..U+042F->U+0430..U+044F, U+0430..U+044F, U+4E00..U+9FFF'), array('min_prefix_len', '0'), array('min_infix_len', '0'), ), @@ -307,7 +307,8 @@ class phpbb_search_fulltext_sphinx array('mem_limit', $config['fulltext_sphinx_indexer_mem_limit'] . 'M'), ), 'searchd' => array( - array('address' , '127.0.0.1'), + array('compat_sphinxql_magics' , '0'), + array('listen' , '127.0.0.1'), array('port', ($config['fulltext_sphinx_port']) ? $config['fulltext_sphinx_port'] : '3312'), array('log', $config['fulltext_sphinx_data_path'] . "log/searchd.log"), array('query_log', $config['fulltext_sphinx_data_path'] . "log/sphinx-query.log"), -- cgit v1.2.1 From 8d76bc45ee19186f40dd3b459a9bd33e5e4c23d9 Mon Sep 17 00:00:00 2001 From: Dhruv Goel Date: Tue, 26 Jun 2012 02:35:36 +0530 Subject: [feature/sphinx-fulltext-search] minor fixes in formatting Add a newline at the end of files. Update License information in package docbloc. PHPBB3-10946 --- phpBB/includes/functions_sphinx.php | 5 ++--- phpBB/includes/search/fulltext_sphinx.php | 5 +---- phpBB/includes/sphinxapi-0.9.8.php | 2 +- phpBB/language/en/mods/fulltext_sphinx.php | 2 +- 4 files changed, 5 insertions(+), 9 deletions(-) (limited to 'phpBB') diff --git a/phpBB/includes/functions_sphinx.php b/phpBB/includes/functions_sphinx.php index 976f93f77c..0f83f8cfb5 100644 --- a/phpBB/includes/functions_sphinx.php +++ b/phpBB/includes/functions_sphinx.php @@ -2,9 +2,8 @@ /** * * @package search -* @version $Id$ * @copyright (c) 2005 phpBB Group -* @license http://opensource.org/licenses/gpl-license.php GNU Public License +* @license http://opensource.org/licenses/gpl-2.0.php GNU General Public License v2 * */ @@ -504,4 +503,4 @@ class sphinx_config_comment } } -?> \ No newline at end of file +?> diff --git a/phpBB/includes/search/fulltext_sphinx.php b/phpBB/includes/search/fulltext_sphinx.php index 6c5092f4aa..9ae6438af2 100644 --- a/phpBB/includes/search/fulltext_sphinx.php +++ b/phpBB/includes/search/fulltext_sphinx.php @@ -2,9 +2,8 @@ /** * * @package search -* @version $Id$ * @copyright (c) 2005 phpBB Group -* @license http://opensource.org/licenses/gpl-license.php GNU Public License +* @license http://opensource.org/licenses/gpl-2.0.php GNU General Public License v2 * */ @@ -1167,5 +1166,3 @@ function sphinx_read_last_lines($file, $amount) return $string; } - -?> \ No newline at end of file diff --git a/phpBB/includes/sphinxapi-0.9.8.php b/phpBB/includes/sphinxapi-0.9.8.php index 6a7ea17760..816895d464 100644 --- a/phpBB/includes/sphinxapi-0.9.8.php +++ b/phpBB/includes/sphinxapi-0.9.8.php @@ -1199,4 +1199,4 @@ class SphinxClient // // $Id$ -// \ No newline at end of file +// diff --git a/phpBB/language/en/mods/fulltext_sphinx.php b/phpBB/language/en/mods/fulltext_sphinx.php index e06328afc8..f3fd68aa62 100644 --- a/phpBB/language/en/mods/fulltext_sphinx.php +++ b/phpBB/language/en/mods/fulltext_sphinx.php @@ -62,4 +62,4 @@ $lang = array_merge($lang, array( 'FULLTEXT_SPHINX_STOPWORDS_FILE_EXPLAIN' => 'This setting only works with autoconf enabled. You can place a file called sphinx_stopwords.txt containing one word in each line in your config directory. If this file is present these words will be excluded from the indexing process.', )); -?> \ No newline at end of file +?> -- cgit v1.2.1 From a3b2caf8416c687306b3c2e83b2fdc6e8708cce0 Mon Sep 17 00:00:00 2001 From: Dhruv Goel Date: Mon, 25 Jun 2012 00:03:46 +0530 Subject: [feature/sphinx-fulltext-search] include sample sphinx.conf in docs PHPBB3-10946 --- phpBB/docs/sphinx.sample.conf | 96 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 96 insertions(+) create mode 100644 phpBB/docs/sphinx.sample.conf (limited to 'phpBB') diff --git a/phpBB/docs/sphinx.sample.conf b/phpBB/docs/sphinx.sample.conf new file mode 100644 index 0000000000..d7e59a11fc --- /dev/null +++ b/phpBB/docs/sphinx.sample.conf @@ -0,0 +1,96 @@ +source source_phpbb_{AVATAR_SALT}_main +{ + type = mysql + sql_host = localhost + sql_user = username + sql_pass = password + sql_db = db_name + sql_port = 3306 #optional, default is 3306 + sql_query_range = SELECT MIN(post_id), MAX(post_id) FROM phpbb_posts + sql_range_step = 5000 + sql_query = SELECT \ + p.post_id AS id, \ + p.forum_id, \ + p.topic_id, \ + p.poster_id, \ + IF(p.post_id = t.topic_first_post_id, 1, 0) as topic_first_post, \ + p.post_time, \ + p.post_subject, \ + p.post_subject as title, \ + p.post_text as data, \ + t.topic_last_post_time, \ + 0 as deleted \ + FROM phpbb_posts p, phpbb_topics t \ + WHERE \ + p.topic_id = t.topic_id \ + AND p.post_id >= $start AND p.post_id <= $end + sql_query_post = + sql_query_post_index = REPLACE INTO phpbb_sphinx ( counter_id, max_doc_id ) VALUES ( 1, $maxid ) + sql_query_info = SELECT * FROM phpbb_posts WHERE post_id = $id + sql_query_pre = SET NAMES utf8 + sql_query_pre = REPLACE INTO phpbb_sphinx SELECT 1, MAX(post_id) FROM phpbb_posts + sql_attr_uint = forum_id + sql_attr_uint = topic_id + sql_attr_uint = poster_id + sql_attr_bool = topic_first_post + sql_attr_bool = deleted + sql_attr_timestamp = post_time + sql_attr_timestamp = topic_last_post_time + sql_attr_str2ordinal = post_subject +} +source source_phpbb_{AVATAR_SALT}_delta : source_phpbb_{AVATAR_SALT}_main +{ + sql_query_range = + sql_range_step = + sql_query = SELECT \ + p.post_id AS id, \ + p.forum_id, \ + p.topic_id, \ + p.poster_id, \ + IF(p.post_id = t.topic_first_post_id, 1, 0) as topic_first_post, \ + p.post_time, \ + p.post_subject, \ + p.post_subject as title, \ + p.post_text as data, \ + t.topic_last_post_time, \ + 0 as deleted \ + FROM phpbb_posts p, phpbb_topics t \ + WHERE \ + p.topic_id = t.topic_id \ + AND p.post_id >= ( SELECT max_doc_id FROM phpbb_sphinx WHERE counter_id=1 ) + sql_query_pre = +} +index index_phpbb_{AVATAR_SALT}_main +{ + path = {DATA_PATH}/index_phpbb_{AVATAR_SALT}_main + source = source_phpbb_{AVATAR_SALT}_main + docinfo = extern + morphology = none + stopwords = + min_word_len = 2 + charset_type = utf-8 + charset_table = U+FF10..U+FF19->0..9, 0..9, U+FF41..U+FF5A->a..z, U+FF21..U+FF3A->a..z, A..Z->a..z, a..z, U+0149, U+017F, U+0138, U+00DF, U+00FF, U+00C0..U+00D6->U+00E0..U+00F6, U+00E0..U+00F6, U+00D8..U+00DE->U+00F8..U+00FE, U+00F8..U+00FE, U+0100->U+0101, U+0101, U+0102->U+0103, U+0103, U+0104->U+0105, U+0105, U+0106->U+0107, U+0107, U+0108->U+0109, U+0109, U+010A->U+010B, U+010B, U+010C->U+010D, U+010D, U+010E->U+010F, U+010F, U+0110->U+0111, U+0111, U+0112->U+0113, U+0113, U+0114->U+0115, U+0115, U+0116->U+0117, U+0117, U+0118->U+0119, U+0119, U+011A->U+011B, U+011B, U+011C->U+011D, U+011D, U+011E->U+011F, U+011F, U+0130->U+0131, U+0131, U+0132->U+0133, U+0133, U+0134->U+0135, U+0135, U+0136->U+0137, U+0137, U+0139->U+013A, U+013A, U+013B->U+013C, U+013C, U+013D->U+013E, U+013E, U+013F->U+0140, U+0140, U+0141->U+0142, U+0142, U+0143->U+0144, U+0144, U+0145->U+0146, U+0146, U+0147->U+0148, U+0148, U+014A->U+014B, U+014B, U+014C->U+014D, U+014D, U+014E->U+014F, U+014F, U+0150->U+0151, U+0151, U+0152->U+0153, U+0153, U+0154->U+0155, U+0155, U+0156->U+0157, U+0157, U+0158->U+0159, U+0159, U+015A->U+015B, U+015B, U+015C->U+015D, U+015D, U+015E->U+015F, U+015F, U+0160->U+0161, U+0161, U+0162->U+0163, U+0163, U+0164->U+0165, U+0165, U+0166->U+0167, U+0167, U+0168->U+0169, U+0169, U+016A->U+016B, U+016B, U+016C->U+016D, U+016D, U+016E->U+016F, U+016F, U+0170->U+0171, U+0171, U+0172->U+0173, U+0173, U+0174->U+0175, U+0175, U+0176->U+0177, U+0177, U+0178->U+00FF, U+00FF, U+0179->U+017A, U+017A, U+017B->U+017C, U+017C, U+017D->U+017E, U+017E, U+0410..U+042F->U+0430..U+044F, U+0430..U+044F, U+4E00..U+9FFF + min_prefix_len = 0 + min_infix_len = 0 +} +index index_phpbb_{AVATAR_SALT}_delta : index_phpbb_{AVATAR_SALT}_main +{ + path = {DATA_PATH}/index_phpbb_{AVATAR_SALT}_delta + source = source_phpbb_{AVATAR_SALT}_delta +} +indexer +{ + mem_limit = 512M +} +searchd +{ + compat_sphinxql_magics = 0 + listen = 127.0.0.1 + port = 3312 + log = {DATA_PATH}/log/searchd.log + query_log = {DATA_PATH}/log/sphinx-query.log + read_timeout = 5 + max_children = 30 + pid_file = {DATA_PATH}/searchd.pid + max_matches = 20000 +} -- cgit v1.2.1 From 455a35d8361c93657874e140a2ad5b2e5c267757 Mon Sep 17 00:00:00 2001 From: Dhruv Goel Date: Tue, 26 Jun 2012 03:56:03 +0530 Subject: [feature/sphinx-fulltext-search] temporary commit to pull out sphinx-api also need to add the latest sphinx api instead of this. PHPBB3-10946 --- phpBB/includes/sphinxapi-0.9.8.php | 1202 ------------------------------------ 1 file changed, 1202 deletions(-) delete mode 100644 phpBB/includes/sphinxapi-0.9.8.php (limited to 'phpBB') diff --git a/phpBB/includes/sphinxapi-0.9.8.php b/phpBB/includes/sphinxapi-0.9.8.php deleted file mode 100644 index 816895d464..0000000000 --- a/phpBB/includes/sphinxapi-0.9.8.php +++ /dev/null @@ -1,1202 +0,0 @@ -=8 ) - { - $i = (int)$v; - return pack ( "NN", $i>>32, $i&((1<<32)-1) ); - } - - // x32 route, bcmath - $x = "4294967296"; - if ( function_exists("bcmul") ) - { - $h = bcdiv ( $v, $x, 0 ); - $l = bcmod ( $v, $x ); - return pack ( "NN", (float)$h, (float)$l ); // conversion to float is intentional; int would lose 31st bit - } - - // x32 route, 15 or less decimal digits - // we can use float, because its actually double and has 52 precision bits - if ( strlen($v)<=15 ) - { - $f = (float)$v; - $h = (int)($f/$x); - $l = (int)($f-$x*$h); - return pack ( "NN", $h, $l ); - } - - // x32 route, 16 or more decimal digits - // well, let me know if you *really* need this - die ( "INTERNAL ERROR: packing more than 15-digit numeric on 32-bit PHP is not implemented yet (contact support)" ); -} - - -/// portably unpack 64 unsigned bits, network order to numeric -function sphUnpack64 ( $v ) -{ - list($h,$l) = array_values ( unpack ( "N*N*", $v ) ); - - // x64 route - if ( PHP_INT_SIZE>=8 ) - { - if ( $h<0 ) $h += (1<<32); // because php 5.2.2 to 5.2.5 is totally fucked up again - if ( $l<0 ) $l += (1<<32); - return ($h<<32) + $l; - } - - // x32 route - $h = sprintf ( "%u", $h ); - $l = sprintf ( "%u", $l ); - $x = "4294967296"; - - // bcmath - if ( function_exists("bcmul") ) - return bcadd ( $l, bcmul ( $x, $h ) ); - - // no bcmath, 15 or less decimal digits - // we can use float, because its actually double and has 52 precision bits - if ( $h<1048576 ) - { - $f = ((float)$h)*$x + (float)$l; - return sprintf ( "%.0f", $f ); // builtin conversion is only about 39-40 bits precise! - } - - // x32 route, 16 or more decimal digits - // well, let me know if you *really* need this - die ( "INTERNAL ERROR: unpacking more than 15-digit numeric on 32-bit PHP is not implemented yet (contact support)" ); -} - - -/// sphinx searchd client class -class SphinxClient -{ - var $_host; ///< searchd host (default is "localhost") - var $_port; ///< searchd port (default is 3312) - var $_offset; ///< how many records to seek from result-set start (default is 0) - var $_limit; ///< how many records to return from result-set starting at offset (default is 20) - var $_mode; ///< query matching mode (default is SPH_MATCH_ALL) - var $_weights; ///< per-field weights (default is 1 for all fields) - var $_sort; ///< match sorting mode (default is SPH_SORT_RELEVANCE) - var $_sortby; ///< attribute to sort by (defualt is "") - var $_min_id; ///< min ID to match (default is 0, which means no limit) - var $_max_id; ///< max ID to match (default is 0, which means no limit) - var $_filters; ///< search filters - var $_groupby; ///< group-by attribute name - var $_groupfunc; ///< group-by function (to pre-process group-by attribute value with) - var $_groupsort; ///< group-by sorting clause (to sort groups in result set with) - var $_groupdistinct;///< group-by count-distinct attribute - var $_maxmatches; ///< max matches to retrieve - var $_cutoff; ///< cutoff to stop searching at (default is 0) - var $_retrycount; ///< distributed retries count - var $_retrydelay; ///< distributed retries delay - var $_anchor; ///< geographical anchor point - var $_indexweights; ///< per-index weights - var $_ranker; ///< ranking mode (default is SPH_RANK_PROXIMITY_BM25) - var $_maxquerytime; ///< max query time, milliseconds (default is 0, do not limit) - var $_fieldweights; ///< per-field-name weights - - var $_error; ///< last error message - var $_warning; ///< last warning message - - var $_reqs; ///< requests array for multi-query - var $_mbenc; ///< stored mbstring encoding - var $_arrayresult; ///< whether $result["matches"] should be a hash or an array - var $_timeout; ///< connect timeout - - ///////////////////////////////////////////////////////////////////////////// - // common stuff - ///////////////////////////////////////////////////////////////////////////// - - /// create a new client object and fill defaults - function SphinxClient () - { - // per-client-object settings - $this->_host = "localhost"; - $this->_port = 3312; - - // per-query settings - $this->_offset = 0; - $this->_limit = 20; - $this->_mode = SPH_MATCH_ALL; - $this->_weights = array (); - $this->_sort = SPH_SORT_RELEVANCE; - $this->_sortby = ""; - $this->_min_id = 0; - $this->_max_id = 0; - $this->_filters = array (); - $this->_groupby = ""; - $this->_groupfunc = SPH_GROUPBY_DAY; - $this->_groupsort = "@group desc"; - $this->_groupdistinct= ""; - $this->_maxmatches = 1000; - $this->_cutoff = 0; - $this->_retrycount = 0; - $this->_retrydelay = 0; - $this->_anchor = array (); - $this->_indexweights= array (); - $this->_ranker = SPH_RANK_PROXIMITY_BM25; - $this->_maxquerytime= 0; - $this->_fieldweights= array(); - - $this->_error = ""; // per-reply fields (for single-query case) - $this->_warning = ""; - $this->_reqs = array (); // requests storage (for multi-query case) - $this->_mbenc = ""; - $this->_arrayresult = false; - $this->_timeout = 0; - } - - /// get last error message (string) - function GetLastError () - { - return $this->_error; - } - - /// get last warning message (string) - function GetLastWarning () - { - return $this->_warning; - } - - /// set searchd host name (string) and port (integer) - function SetServer ( $host, $port ) - { - assert ( is_string($host) ); - assert ( is_int($port) ); - $this->_host = $host; - $this->_port = $port; - } - - /// set server connection timeout (0 to remove) - function SetConnectTimeout ( $timeout ) - { - assert ( is_numeric($timeout) ); - $this->_timeout = $timeout; - } - - ///////////////////////////////////////////////////////////////////////////// - - /// enter mbstring workaround mode - function _MBPush () - { - $this->_mbenc = ""; - if ( ini_get ( "mbstring.func_overload" ) & 2 ) - { - $this->_mbenc = mb_internal_encoding(); - mb_internal_encoding ( "latin1" ); - } - } - - /// leave mbstring workaround mode - function _MBPop () - { - if ( $this->_mbenc ) - mb_internal_encoding ( $this->_mbenc ); - } - - /// connect to searchd server - function _Connect ($allow_retry = true) - { - $errno = 0; - $errstr = ""; - if ( $this->_timeout<=0 ) - $fp = @fsockopen ( $this->_host, $this->_port, $errno, $errstr ); - else - $fp = @fsockopen ( $this->_host, $this->_port, $errno, $errstr, $this->_timeout ); - - if ( !$fp ) - { - $errstr = trim ( $errstr ); - $this->_error = "connection to {$this->_host}:{$this->_port} failed (errno=$errno, msg=$errstr)"; - return false; - } - - // check version - //list(,$v) = unpack ( "N*", fread ( $fp, 4 ) ); - $version_data = unpack ( "N*", fread ( $fp, 4 ) ); - if (!isset($version_data[1])) - { - // this should not happen, try to reconnect ONCE - if ($allow_retry) - { - return $this->_Connect(false); - } - else - { - $this->_error = "unexpected version data"; - return false; - } - } - $v = $version_data[1]; - $v = (int)$v; - if ( $v<1 ) - { - fclose ( $fp ); - $this->_error = "expected searchd protocol version 1+, got version '$v'"; - return false; - } - - // all ok, send my version - fwrite ( $fp, pack ( "N", 1 ) ); - return $fp; - } - - /// get and check response packet from searchd server - function _GetResponse ( $fp, $client_ver ) - { - $response = ""; - $len = 0; - - $header = fread ( $fp, 8 ); - if ( strlen($header)==8 ) - { - list ( $status, $ver, $len ) = array_values ( unpack ( "n2a/Nb", $header ) ); - $left = $len; - while ( $left>0 && !feof($fp) ) - { - $chunk = fread ( $fp, $left ); - if ( $chunk ) - { - $response .= $chunk; - $left -= strlen($chunk); - } - } - } - fclose ( $fp ); - - // check response - $read = strlen ( $response ); - if ( !$response || $read!=$len ) - { - $this->_error = $len - ? "failed to read searchd response (status=$status, ver=$ver, len=$len, read=$read)" - : "received zero-sized searchd response"; - return false; - } - - // check status - if ( $status==SEARCHD_WARNING ) - { - list(,$wlen) = unpack ( "N*", substr ( $response, 0, 4 ) ); - $this->_warning = substr ( $response, 4, $wlen ); - return substr ( $response, 4+$wlen ); - } - if ( $status==SEARCHD_ERROR ) - { - $this->_error = "searchd error: " . substr ( $response, 4 ); - return false; - } - if ( $status==SEARCHD_RETRY ) - { - $this->_error = "temporary searchd error: " . substr ( $response, 4 ); - return false; - } - if ( $status!=SEARCHD_OK ) - { - $this->_error = "unknown status code '$status'"; - return false; - } - - // check version - if ( $ver<$client_ver ) - { - $this->_warning = sprintf ( "searchd command v.%d.%d older than client's v.%d.%d, some options might not work", - $ver>>8, $ver&0xff, $client_ver>>8, $client_ver&0xff ); - } - - return $response; - } - - ///////////////////////////////////////////////////////////////////////////// - // searching - ///////////////////////////////////////////////////////////////////////////// - - /// set offset and count into result set, - /// and optionally set max-matches and cutoff limits - function SetLimits ( $offset, $limit, $max=0, $cutoff=0 ) - { - assert ( is_int($offset) ); - assert ( is_int($limit) ); - assert ( $offset>=0 ); - assert ( $limit>0 ); - assert ( $max>=0 ); - $this->_offset = $offset; - $this->_limit = $limit; - if ( $max>0 ) - $this->_maxmatches = $max; - if ( $cutoff>0 ) - $this->_cutoff = $cutoff; - } - - /// set maximum query time, in milliseconds, per-index - /// integer, 0 means "do not limit" - function SetMaxQueryTime ( $max ) - { - assert ( is_int($max) ); - assert ( $max>=0 ); - $this->_maxquerytime = $max; - } - - /// set matching mode - function SetMatchMode ( $mode ) - { - assert ( $mode==SPH_MATCH_ALL - || $mode==SPH_MATCH_ANY - || $mode==SPH_MATCH_PHRASE - || $mode==SPH_MATCH_BOOLEAN - || $mode==SPH_MATCH_EXTENDED - || $mode==SPH_MATCH_FULLSCAN - || $mode==SPH_MATCH_EXTENDED2 ); - $this->_mode = $mode; - } - - /// set ranking mode - function SetRankingMode ( $ranker ) - { - assert ( $ranker==SPH_RANK_PROXIMITY_BM25 - || $ranker==SPH_RANK_BM25 - || $ranker==SPH_RANK_NONE - || $ranker==SPH_RANK_WORDCOUNT ); - $this->_ranker = $ranker; - } - - /// set matches sorting mode - function SetSortMode ( $mode, $sortby="" ) - { - assert ( - $mode==SPH_SORT_RELEVANCE || - $mode==SPH_SORT_ATTR_DESC || - $mode==SPH_SORT_ATTR_ASC || - $mode==SPH_SORT_TIME_SEGMENTS || - $mode==SPH_SORT_EXTENDED || - $mode==SPH_SORT_EXPR ); - assert ( is_string($sortby) ); - assert ( $mode==SPH_SORT_RELEVANCE || strlen($sortby)>0 ); - - $this->_sort = $mode; - $this->_sortby = $sortby; - } - - /// bind per-field weights by order - /// DEPRECATED; use SetFieldWeights() instead - function SetWeights ( $weights ) - { - assert ( is_array($weights) ); - foreach ( $weights as $weight ) - assert ( is_int($weight) ); - - $this->_weights = $weights; - } - - /// bind per-field weights by name - function SetFieldWeights ( $weights ) - { - assert ( is_array($weights) ); - foreach ( $weights as $name=>$weight ) - { - assert ( is_string($name) ); - assert ( is_int($weight) ); - } - $this->_fieldweights = $weights; - } - - /// bind per-index weights by name - function SetIndexWeights ( $weights ) - { - assert ( is_array($weights) ); - foreach ( $weights as $index=>$weight ) - { - assert ( is_string($index) ); - assert ( is_int($weight) ); - } - $this->_indexweights = $weights; - } - - /// set IDs range to match - /// only match records if document ID is beetwen $min and $max (inclusive) - function SetIDRange ( $min, $max ) - { - assert ( is_numeric($min) ); - assert ( is_numeric($max) ); - assert ( $min<=$max ); - $this->_min_id = $min; - $this->_max_id = $max; - } - - /// set values set filter - /// only match records where $attribute value is in given set - function SetFilter ( $attribute, $values, $exclude=false ) - { - assert ( is_string($attribute) ); - assert ( is_array($values) ); - assert ( count($values) ); - - if ( is_array($values) && count($values) ) - { - foreach ( $values as $value ) - assert ( is_numeric($value) ); - - $this->_filters[] = array ( "type"=>SPH_FILTER_VALUES, "attr"=>$attribute, "exclude"=>$exclude, "values"=>$values ); - } - } - - /// set range filter - /// only match records if $attribute value is beetwen $min and $max (inclusive) - function SetFilterRange ( $attribute, $min, $max, $exclude=false ) - { - assert ( is_string($attribute) ); - assert ( is_int($min) ); - assert ( is_int($max) ); - assert ( $min<=$max ); - - $this->_filters[] = array ( "type"=>SPH_FILTER_RANGE, "attr"=>$attribute, "exclude"=>$exclude, "min"=>$min, "max"=>$max ); - } - - /// set float range filter - /// only match records if $attribute value is beetwen $min and $max (inclusive) - function SetFilterFloatRange ( $attribute, $min, $max, $exclude=false ) - { - assert ( is_string($attribute) ); - assert ( is_float($min) ); - assert ( is_float($max) ); - assert ( $min<=$max ); - - $this->_filters[] = array ( "type"=>SPH_FILTER_FLOATRANGE, "attr"=>$attribute, "exclude"=>$exclude, "min"=>$min, "max"=>$max ); - } - - /// setup anchor point for geosphere distance calculations - /// required to use @geodist in filters and sorting - /// latitude and longitude must be in radians - function SetGeoAnchor ( $attrlat, $attrlong, $lat, $long ) - { - assert ( is_string($attrlat) ); - assert ( is_string($attrlong) ); - assert ( is_float($lat) ); - assert ( is_float($long) ); - - $this->_anchor = array ( "attrlat"=>$attrlat, "attrlong"=>$attrlong, "lat"=>$lat, "long"=>$long ); - } - - /// set grouping attribute and function - function SetGroupBy ( $attribute, $func, $groupsort="@group desc" ) - { - assert ( is_string($attribute) ); - assert ( is_string($groupsort) ); - assert ( $func==SPH_GROUPBY_DAY - || $func==SPH_GROUPBY_WEEK - || $func==SPH_GROUPBY_MONTH - || $func==SPH_GROUPBY_YEAR - || $func==SPH_GROUPBY_ATTR - || $func==SPH_GROUPBY_ATTRPAIR ); - - $this->_groupby = $attribute; - $this->_groupfunc = $func; - $this->_groupsort = $groupsort; - } - - /// set count-distinct attribute for group-by queries - function SetGroupDistinct ( $attribute ) - { - assert ( is_string($attribute) ); - $this->_groupdistinct = $attribute; - } - - /// set distributed retries count and delay - function SetRetries ( $count, $delay=0 ) - { - assert ( is_int($count) && $count>=0 ); - assert ( is_int($delay) && $delay>=0 ); - $this->_retrycount = $count; - $this->_retrydelay = $delay; - } - - /// set result set format (hash or array; hash by default) - /// PHP specific; needed for group-by-MVA result sets that may contain duplicate IDs - function SetArrayResult ( $arrayresult ) - { - assert ( is_bool($arrayresult) ); - $this->_arrayresult = $arrayresult; - } - - ////////////////////////////////////////////////////////////////////////////// - - /// clear all filters (for multi-queries) - function ResetFilters () - { - $this->_filters = array(); - $this->_anchor = array(); - } - - /// clear groupby settings (for multi-queries) - function ResetGroupBy () - { - $this->_groupby = ""; - $this->_groupfunc = SPH_GROUPBY_DAY; - $this->_groupsort = "@group desc"; - $this->_groupdistinct= ""; - } - - ////////////////////////////////////////////////////////////////////////////// - - /// connect to searchd server, run given search query through given indexes, - /// and return the search results - function Query ( $query, $index="*", $comment="" ) - { - assert ( empty($this->_reqs) ); - - $this->AddQuery ( $query, $index, $comment ); - $results = $this->RunQueries (); - $this->_reqs = array (); // just in case it failed too early - - if ( !is_array($results) ) - return false; // probably network error; error message should be already filled - - $this->_error = $results[0]["error"]; - $this->_warning = $results[0]["warning"]; - if ( $results[0]["status"]==SEARCHD_ERROR ) - return false; - else - return $results[0]; - } - - /// helper to pack floats in network byte order - function _PackFloat ( $f ) - { - $t1 = pack ( "f", $f ); // machine order - list(,$t2) = unpack ( "L*", $t1 ); // int in machine order - return pack ( "N", $t2 ); - } - - /// add query to multi-query batch - /// returns index into results array from RunQueries() call - function AddQuery ( $query, $index="*", $comment="" ) - { - // mbstring workaround - $this->_MBPush (); - - // build request - $req = pack ( "NNNNN", $this->_offset, $this->_limit, $this->_mode, $this->_ranker, $this->_sort ); // mode and limits - $req .= pack ( "N", strlen($this->_sortby) ) . $this->_sortby; - $req .= pack ( "N", strlen($query) ) . $query; // query itself - $req .= pack ( "N", count($this->_weights) ); // weights - foreach ( $this->_weights as $weight ) - $req .= pack ( "N", (int)$weight ); - $req .= pack ( "N", strlen($index) ) . $index; // indexes - $req .= pack ( "N", 1 ); // id64 range marker - $req .= sphPack64 ( $this->_min_id ) . sphPack64 ( $this->_max_id ); // id64 range - - // filters - $req .= pack ( "N", count($this->_filters) ); - foreach ( $this->_filters as $filter ) - { - $req .= pack ( "N", strlen($filter["attr"]) ) . $filter["attr"]; - $req .= pack ( "N", $filter["type"] ); - switch ( $filter["type"] ) - { - case SPH_FILTER_VALUES: - $req .= pack ( "N", count($filter["values"]) ); - foreach ( $filter["values"] as $value ) - $req .= pack ( "N", floatval($value) ); // this uberhack is to workaround 32bit signed int limit on x32 platforms - break; - - case SPH_FILTER_RANGE: - $req .= pack ( "NN", $filter["min"], $filter["max"] ); - break; - - case SPH_FILTER_FLOATRANGE: - $req .= $this->_PackFloat ( $filter["min"] ) . $this->_PackFloat ( $filter["max"] ); - break; - - default: - assert ( 0 && "internal error: unhandled filter type" ); - } - $req .= pack ( "N", $filter["exclude"] ); - } - - // group-by clause, max-matches count, group-sort clause, cutoff count - $req .= pack ( "NN", $this->_groupfunc, strlen($this->_groupby) ) . $this->_groupby; - $req .= pack ( "N", $this->_maxmatches ); - $req .= pack ( "N", strlen($this->_groupsort) ) . $this->_groupsort; - $req .= pack ( "NNN", $this->_cutoff, $this->_retrycount, $this->_retrydelay ); - $req .= pack ( "N", strlen($this->_groupdistinct) ) . $this->_groupdistinct; - - // anchor point - if ( empty($this->_anchor) ) - { - $req .= pack ( "N", 0 ); - } else - { - $a =& $this->_anchor; - $req .= pack ( "N", 1 ); - $req .= pack ( "N", strlen($a["attrlat"]) ) . $a["attrlat"]; - $req .= pack ( "N", strlen($a["attrlong"]) ) . $a["attrlong"]; - $req .= $this->_PackFloat ( $a["lat"] ) . $this->_PackFloat ( $a["long"] ); - } - - // per-index weights - $req .= pack ( "N", count($this->_indexweights) ); - foreach ( $this->_indexweights as $idx=>$weight ) - $req .= pack ( "N", strlen($idx) ) . $idx . pack ( "N", $weight ); - - // max query time - $req .= pack ( "N", $this->_maxquerytime ); - - // per-field weights - $req .= pack ( "N", count($this->_fieldweights) ); - foreach ( $this->_fieldweights as $field=>$weight ) - $req .= pack ( "N", strlen($field) ) . $field . pack ( "N", $weight ); - - // comment - $req .= pack ( "N", strlen($comment) ) . $comment; - - // mbstring workaround - $this->_MBPop (); - - // store request to requests array - $this->_reqs[] = $req; - return count($this->_reqs)-1; - } - - /// connect to searchd, run queries batch, and return an array of result sets - function RunQueries () - { - if ( empty($this->_reqs) ) - { - $this->_error = "no queries defined, issue AddQuery() first"; - return false; - } - - // mbstring workaround - $this->_MBPush (); - - if (!( $fp = $this->_Connect() )) - { - $this->_MBPop (); - return false; - } - - //////////////////////////// - // send query, get response - //////////////////////////// - - $nreqs = count($this->_reqs); - $req = join ( "", $this->_reqs ); - $len = 4+strlen($req); - $req = pack ( "nnNN", SEARCHD_COMMAND_SEARCH, VER_COMMAND_SEARCH, $len, $nreqs ) . $req; // add header - - fwrite ( $fp, $req, $len+8 ); - if (!( $response = $this->_GetResponse ( $fp, VER_COMMAND_SEARCH ) )) - { - $this->_MBPop (); - return false; - } - - $this->_reqs = array (); - - ////////////////// - // parse response - ////////////////// - - $p = 0; // current position - $max = strlen($response); // max position for checks, to protect against broken responses - - $results = array (); - for ( $ires=0; $ires<$nreqs && $p<$max; $ires++ ) - { - $results[] = array(); - $result =& $results[$ires]; - - $result["error"] = ""; - $result["warning"] = ""; - - // extract status - list(,$status) = unpack ( "N*", substr ( $response, $p, 4 ) ); $p += 4; - $result["status"] = $status; - if ( $status!=SEARCHD_OK ) - { - list(,$len) = unpack ( "N*", substr ( $response, $p, 4 ) ); $p += 4; - $message = substr ( $response, $p, $len ); $p += $len; - - if ( $status==SEARCHD_WARNING ) - { - $result["warning"] = $message; - } else - { - $result["error"] = $message; - continue; - } - } - - // read schema - $fields = array (); - $attrs = array (); - - list(,$nfields) = unpack ( "N*", substr ( $response, $p, 4 ) ); $p += 4; - while ( $nfields-->0 && $p<$max ) - { - list(,$len) = unpack ( "N*", substr ( $response, $p, 4 ) ); $p += 4; - $fields[] = substr ( $response, $p, $len ); $p += $len; - } - $result["fields"] = $fields; - - list(,$nattrs) = unpack ( "N*", substr ( $response, $p, 4 ) ); $p += 4; - while ( $nattrs-->0 && $p<$max ) - { - list(,$len) = unpack ( "N*", substr ( $response, $p, 4 ) ); $p += 4; - $attr = substr ( $response, $p, $len ); $p += $len; - list(,$type) = unpack ( "N*", substr ( $response, $p, 4 ) ); $p += 4; - $attrs[$attr] = $type; - } - $result["attrs"] = $attrs; - - // read match count - list(,$count) = unpack ( "N*", substr ( $response, $p, 4 ) ); $p += 4; - list(,$id64) = unpack ( "N*", substr ( $response, $p, 4 ) ); $p += 4; - - // read matches - $idx = -1; - while ( $count-->0 && $p<$max ) - { - // index into result array - $idx++; - - // parse document id and weight - if ( $id64 ) - { - $doc = sphUnpack64 ( substr ( $response, $p, 8 ) ); $p += 8; - list(,$weight) = unpack ( "N*", substr ( $response, $p, 4 ) ); $p += 4; - } else - { - list ( $doc, $weight ) = array_values ( unpack ( "N*N*", - substr ( $response, $p, 8 ) ) ); - $p += 8; - - if ( PHP_INT_SIZE>=8 ) - { - // x64 route, workaround broken unpack() in 5.2.2+ - if ( $doc<0 ) $doc += (1<<32); - } else - { - // x32 route, workaround php signed/unsigned braindamage - $doc = sprintf ( "%u", $doc ); - } - } - $weight = sprintf ( "%u", $weight ); - - // create match entry - if ( $this->_arrayresult ) - $result["matches"][$idx] = array ( "id"=>$doc, "weight"=>$weight ); - else - $result["matches"][$doc]["weight"] = $weight; - - // parse and create attributes - $attrvals = array (); - foreach ( $attrs as $attr=>$type ) - { - // handle floats - if ( $type==SPH_ATTR_FLOAT ) - { - list(,$uval) = unpack ( "N*", substr ( $response, $p, 4 ) ); $p += 4; - list(,$fval) = unpack ( "f*", pack ( "L", $uval ) ); - $attrvals[$attr] = $fval; - continue; - } - - // handle everything else as unsigned ints - list(,$val) = unpack ( "N*", substr ( $response, $p, 4 ) ); $p += 4; - if ( $type & SPH_ATTR_MULTI ) - { - $attrvals[$attr] = array (); - $nvalues = $val; - while ( $nvalues-->0 && $p<$max ) - { - list(,$val) = unpack ( "N*", substr ( $response, $p, 4 ) ); $p += 4; - $attrvals[$attr][] = sprintf ( "%u", $val ); - } - } else - { - $attrvals[$attr] = sprintf ( "%u", $val ); - } - } - - if ( $this->_arrayresult ) - $result["matches"][$idx]["attrs"] = $attrvals; - else - $result["matches"][$doc]["attrs"] = $attrvals; - } - - list ( $total, $total_found, $msecs, $words ) = - array_values ( unpack ( "N*N*N*N*", substr ( $response, $p, 16 ) ) ); - $result["total"] = sprintf ( "%u", $total ); - $result["total_found"] = sprintf ( "%u", $total_found ); - $result["time"] = sprintf ( "%.3f", $msecs/1000 ); - $p += 16; - - while ( $words-->0 && $p<$max ) - { - list(,$len) = unpack ( "N*", substr ( $response, $p, 4 ) ); $p += 4; - $word = substr ( $response, $p, $len ); $p += $len; - list ( $docs, $hits ) = array_values ( unpack ( "N*N*", substr ( $response, $p, 8 ) ) ); $p += 8; - $result["words"][$word] = array ( - "docs"=>sprintf ( "%u", $docs ), - "hits"=>sprintf ( "%u", $hits ) ); - } - } - - $this->_MBPop (); - return $results; - } - - ///////////////////////////////////////////////////////////////////////////// - // excerpts generation - ///////////////////////////////////////////////////////////////////////////// - - /// connect to searchd server, and generate exceprts (snippets) - /// of given documents for given query. returns false on failure, - /// an array of snippets on success - function BuildExcerpts ( $docs, $index, $words, $opts=array() ) - { - assert ( is_array($docs) ); - assert ( is_string($index) ); - assert ( is_string($words) ); - assert ( is_array($opts) ); - - $this->_MBPush (); - - if (!( $fp = $this->_Connect() )) - { - $this->_MBPop(); - return false; - } - - ///////////////// - // fixup options - ///////////////// - - if ( !isset($opts["before_match"]) ) $opts["before_match"] = ""; - if ( !isset($opts["after_match"]) ) $opts["after_match"] = ""; - if ( !isset($opts["chunk_separator"]) ) $opts["chunk_separator"] = " ... "; - if ( !isset($opts["limit"]) ) $opts["limit"] = 256; - if ( !isset($opts["around"]) ) $opts["around"] = 5; - if ( !isset($opts["exact_phrase"]) ) $opts["exact_phrase"] = false; - if ( !isset($opts["single_passage"]) ) $opts["single_passage"] = false; - if ( !isset($opts["use_boundaries"]) ) $opts["use_boundaries"] = false; - if ( !isset($opts["weight_order"]) ) $opts["weight_order"] = false; - - ///////////////// - // build request - ///////////////// - - // v.1.0 req - $flags = 1; // remove spaces - if ( $opts["exact_phrase"] ) $flags |= 2; - if ( $opts["single_passage"] ) $flags |= 4; - if ( $opts["use_boundaries"] ) $flags |= 8; - if ( $opts["weight_order"] ) $flags |= 16; - $req = pack ( "NN", 0, $flags ); // mode=0, flags=$flags - $req .= pack ( "N", strlen($index) ) . $index; // req index - $req .= pack ( "N", strlen($words) ) . $words; // req words - - // options - $req .= pack ( "N", strlen($opts["before_match"]) ) . $opts["before_match"]; - $req .= pack ( "N", strlen($opts["after_match"]) ) . $opts["after_match"]; - $req .= pack ( "N", strlen($opts["chunk_separator"]) ) . $opts["chunk_separator"]; - $req .= pack ( "N", (int)$opts["limit"] ); - $req .= pack ( "N", (int)$opts["around"] ); - - // documents - $req .= pack ( "N", count($docs) ); - foreach ( $docs as $doc ) - { - assert ( is_string($doc) ); - $req .= pack ( "N", strlen($doc) ) . $doc; - } - - //////////////////////////// - // send query, get response - //////////////////////////// - - $len = strlen($req); - $req = pack ( "nnN", SEARCHD_COMMAND_EXCERPT, VER_COMMAND_EXCERPT, $len ) . $req; // add header - $wrote = fwrite ( $fp, $req, $len+8 ); - if (!( $response = $this->_GetResponse ( $fp, VER_COMMAND_EXCERPT ) )) - { - $this->_MBPop (); - return false; - } - - ////////////////// - // parse response - ////////////////// - - $pos = 0; - $res = array (); - $rlen = strlen($response); - for ( $i=0; $i $rlen ) - { - $this->_error = "incomplete reply"; - $this->_MBPop (); - return false; - } - $res[] = $len ? substr ( $response, $pos, $len ) : ""; - $pos += $len; - } - - $this->_MBPop (); - return $res; - } - - - ///////////////////////////////////////////////////////////////////////////// - // keyword generation - ///////////////////////////////////////////////////////////////////////////// - - /// connect to searchd server, and generate keyword list for a given query - /// returns false on failure, - /// an array of words on success - function BuildKeywords ( $query, $index, $hits ) - { - assert ( is_string($query) ); - assert ( is_string($index) ); - assert ( is_bool($hits) ); - - $this->_MBPush (); - - if (!( $fp = $this->_Connect() )) - { - $this->_MBPop(); - return false; - } - - ///////////////// - // build request - ///////////////// - - // v.1.0 req - $req = pack ( "N", strlen($query) ) . $query; // req query - $req .= pack ( "N", strlen($index) ) . $index; // req index - $req .= pack ( "N", (int)$hits ); - - //////////////////////////// - // send query, get response - //////////////////////////// - - $len = strlen($req); - $req = pack ( "nnN", SEARCHD_COMMAND_KEYWORDS, VER_COMMAND_KEYWORDS, $len ) . $req; // add header - $wrote = fwrite ( $fp, $req, $len+8 ); - if (!( $response = $this->_GetResponse ( $fp, VER_COMMAND_KEYWORDS ) )) - { - $this->_MBPop (); - return false; - } - - ////////////////// - // parse response - ////////////////// - - $pos = 0; - $res = array (); - $rlen = strlen($response); - list(,$nwords) = unpack ( "N*", substr ( $response, $pos, 4 ) ); - $pos += 4; - for ( $i=0; $i<$nwords; $i++ ) - { - list(,$len) = unpack ( "N*", substr ( $response, $pos, 4 ) ); $pos += 4; - $tokenized = $len ? substr ( $response, $pos, $len ) : ""; - $pos += $len; - - list(,$len) = unpack ( "N*", substr ( $response, $pos, 4 ) ); $pos += 4; - $normalized = $len ? substr ( $response, $pos, $len ) : ""; - $pos += $len; - - $res[] = array ( "tokenized"=>$tokenized, "normalized"=>$normalized ); - - if ( $hits ) - { - list($ndocs,$nhits) = array_values ( unpack ( "N*N*", substr ( $response, $pos, 8 ) ) ); - $pos += 8; - $res [$i]["docs"] = $ndocs; - $res [$i]["hits"] = $nhits; - } - - if ( $pos > $rlen ) - { - $this->_error = "incomplete reply"; - $this->_MBPop (); - return false; - } - } - - $this->_MBPop (); - return $res; - } - - function EscapeString ( $string ) - { - $from = array ( '(',')','|','-','!','@','~','"','&', '/' ); - $to = array ( '\(','\)','\|','\-','\!','\@','\~','\"', '\&', '\/' ); - - return str_replace ( $from, $to, $string ); - } - - ///////////////////////////////////////////////////////////////////////////// - // attribute updates - ///////////////////////////////////////////////////////////////////////////// - - /// update given attribute values on given documents in given indexes - /// returns amount of updated documents (0 or more) on success, or -1 on failure - function UpdateAttributes ( $index, $attrs, $values ) - { - // verify everything - assert ( is_string($index) ); - - assert ( is_array($attrs) ); - foreach ( $attrs as $attr ) - assert ( is_string($attr) ); - - assert ( is_array($values) ); - foreach ( $values as $id=>$entry ) - { - assert ( is_numeric($id) ); - assert ( is_array($entry) ); - assert ( count($entry)==count($attrs) ); - foreach ( $entry as $v ) - assert ( is_int($v) ); - } - - // build request - $req = pack ( "N", strlen($index) ) . $index; - - $req .= pack ( "N", count($attrs) ); - foreach ( $attrs as $attr ) - $req .= pack ( "N", strlen($attr) ) . $attr; - - $req .= pack ( "N", count($values) ); - foreach ( $values as $id=>$entry ) - { - $req .= sphPack64 ( $id ); - foreach ( $entry as $v ) - $req .= pack ( "N", $v ); - } - - // mbstring workaround - $this->_MBPush (); - - // connect, send query, get response - if (!( $fp = $this->_Connect() )) - { - $this->_MBPop (); - return -1; - } - - $len = strlen($req); - $req = pack ( "nnN", SEARCHD_COMMAND_UPDATE, VER_COMMAND_UPDATE, $len ) . $req; // add header - fwrite ( $fp, $req, $len+8 ); - - if (!( $response = $this->_GetResponse ( $fp, VER_COMMAND_UPDATE ) )) - { - $this->_MBPop (); - return -1; - } - - // parse response - list(,$updated) = unpack ( "N*", substr ( $response, 0, 4 ) ); - $this->_MBPop (); - return $updated; - } -} - -// -// $Id$ -// -- cgit v1.2.1 From 02588069f045ae48984d68c9948c8ecd1c78580d Mon Sep 17 00:00:00 2001 From: Dhruv Goel Date: Tue, 26 Jun 2012 19:06:19 +0530 Subject: [feature/sphinx-fulltext-search] fix config variables config variables now use class property for unique id PHPBB3-10946 Conflicts: phpBB/includes/search/fulltext_sphinx.php --- phpBB/includes/search/fulltext_sphinx.php | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) (limited to 'phpBB') diff --git a/phpBB/includes/search/fulltext_sphinx.php b/phpBB/includes/search/fulltext_sphinx.php index 9ae6438af2..fb16c5639b 100644 --- a/phpBB/includes/search/fulltext_sphinx.php +++ b/phpBB/includes/search/fulltext_sphinx.php @@ -226,7 +226,7 @@ class phpbb_search_fulltext_sphinx $config_object = new sphinx_config($config['fulltext_sphinx_config_path'] . 'sphinx.conf'); $config_data = array( - "source source_phpbb_{$this->id}_main" => array( + 'source source_phpbb_' . $this->id . '_main' => array( array('type', 'mysql'), array('sql_host', $dbhost), array('sql_user', $dbuser), @@ -265,7 +265,7 @@ class phpbb_search_fulltext_sphinx array('sql_attr_timestamp' , 'topic_last_post_time'), array('sql_attr_str2ordinal', 'post_subject'), ), - "source source_phpbb_{$this->id}_delta : source_phpbb_{$this->id}_main" => array( + 'source source_phpbb_' . $this->id . '_delta : source_phpbb_' . $this->id . '_main' => array( array('sql_query_pre', ''), array('sql_query_range', ''), array('sql_range_step', ''), @@ -286,9 +286,9 @@ class phpbb_search_fulltext_sphinx p.topic_id = t.topic_id AND p.post_id >= ( SELECT max_doc_id FROM ' . SPHINX_TABLE . ' WHERE counter_id=1 )'), ), - "index index_phpbb_{$this->id}_main" => array( - array('path', $config['fulltext_sphinx_data_path'] . "index_phpbb_{$this->id}_main"), - array('source', "source_phpbb_{$this->id}_main"), + 'index index_phpbb_' . $this->id . '_main' => array( + array('path', $config['fulltext_sphinx_data_path'] . 'index_phpbb_' . $this->id . '_main'), + array('source', 'source_phpbb_' . $this->id . '_main'), array('docinfo', 'extern'), array('morphology', 'none'), array('stopwords', (file_exists($config['fulltext_sphinx_config_path'] . 'sphinx_stopwords.txt') && $config['fulltext_sphinx_stopwords']) ? $config['fulltext_sphinx_config_path'] . 'sphinx_stopwords.txt' : ''), @@ -298,9 +298,9 @@ class phpbb_search_fulltext_sphinx array('min_prefix_len', '0'), array('min_infix_len', '0'), ), - "index index_phpbb_{$this->id}_delta : index_phpbb_{$this->id}_main" => array( - array('path', $config['fulltext_sphinx_data_path'] . "index_phpbb_{$this->id}_delta"), - array('source', "source_phpbb_{$this->id}_delta"), + 'index index_phpbb_' . $this->id . '_delta : index_phpbb_' . $this->id . '_main' => array( + array('path', $config['fulltext_sphinx_data_path'] . 'index_phpbb_' . $this->id . '_delta'), + array('source', 'source_phpbb_' . $this->id . '_delta'), ), 'indexer' => array( array('mem_limit', $config['fulltext_sphinx_indexer_mem_limit'] . 'M'), -- cgit v1.2.1 From 74a7407927cd5b54328a3941a9926ee35caf17b4 Mon Sep 17 00:00:00 2001 From: Dhruv Goel Date: Wed, 27 Jun 2012 00:17:39 +0530 Subject: [feature/sphinx-fulltext-search] improve classes in functions-sphinx.php PHPBB3-10946 --- phpBB/includes/functions_sphinx.php | 38 +++++++++++++++++-------------------- 1 file changed, 17 insertions(+), 21 deletions(-) (limited to 'phpBB') diff --git a/phpBB/includes/functions_sphinx.php b/phpBB/includes/functions_sphinx.php index 0f83f8cfb5..a4f0e41491 100644 --- a/phpBB/includes/functions_sphinx.php +++ b/phpBB/includes/functions_sphinx.php @@ -46,16 +46,14 @@ class sphinx_config */ function &get_section_by_name($name) { - for ($i = 0, $n = sizeof($this->sections); $i < $n; $i++) + for ($i = 0, $size = sizeof($this->sections); $i < $size; $i++) { // make sure this is really a section object and not a comment - if (is_a($this->sections[$i], 'sphinx_config_section') && $this->sections[$i]->get_name() == $name) + if (($this->sections[$i] instanceof sphinx_config_section) && $this->sections[$i]->get_name() == $name) { return $this->sections[$i]; } } - $null = null; - return $null; } /** @@ -116,7 +114,7 @@ class sphinx_config $section_name = ''; $section_name_comment = ''; $found_opening_bracket = false; - for ($j = 0, $n = strlen($line); $j < $n; $j++) + for ($j = 0, $length = strlen($line); $j < $length; $j++) { if ($line[$j] == '#') { @@ -189,15 +187,15 @@ class sphinx_config $in_value = false; $end_section = false; - // ... then we should prase this line char by char: - // - first there's the variable name - // - then an equal sign - // - the variable value - // - possibly a backslash before the linefeed in this case we need to continue - // parsing the value in the next line - // - a # indicating that the rest of the line is a comment - // - a closing curly bracket indicating the end of this section - for ($j = 0, $n = strlen($line); $j < $n; $j++) + /* ... then we should prase this line char by char: + - first there's the variable name + - then an equal sign + - the variable value + - possibly a backslash before the linefeed in this case we need to continue + parsing the value in the next line + - a # indicating that the rest of the line is a comment + - a closing curly bracket indicating the end of this section*/ + for ($j = 0, $length = strlen($line); $j < $length; $j++) { if ($line[$j] == '#') { @@ -223,7 +221,7 @@ class sphinx_config } else { - if ($line[$j] == '\\' && $j == $n - 1) + if ($line[$j] == '\\' && $j == $length - 1) { $value .= "\n"; $in_value = true; @@ -349,16 +347,14 @@ class sphinx_config_section */ function &get_variable_by_name($name) { - for ($i = 0, $n = sizeof($this->variables); $i < $n; $i++) + for ($i = 0, $size = sizeof($this->variables); $i < $size; $i++) { // make sure this is a variable object and not a comment - if (is_a($this->variables[$i], 'sphinx_config_variable') && $this->variables[$i]->get_name() == $name) + if (($this->variables[$i] instanceof sphinx_config_variable) && $this->variables[$i]->get_name() == $name) { return $this->variables[$i]; } } - $null = null; - return $null; } /** @@ -368,10 +364,10 @@ class sphinx_config_section */ function delete_variables_by_name($name) { - for ($i = 0; $i < sizeof($this->variables); $i++) + for ($i = 0, $size = sizeof($this->variables); $i < $size; $i++) { // make sure this is a variable object and not a comment - if (is_a($this->variables[$i], 'sphinx_config_variable') && $this->variables[$i]->get_name() == $name) + if (($this->variables[$i] instanceof sphinx_config_variable) && $this->variables[$i]->get_name() == $name) { array_splice($this->variables, $i, 1); $i--; -- cgit v1.2.1 From f609555b1ae335b5ea996bf26ee2846058e5256a Mon Sep 17 00:00:00 2001 From: Dhruv Goel Date: Wed, 27 Jun 2012 00:28:31 +0530 Subject: [feature/sphinx-fulltext-search] integrate sphinx language keys with core Language keys removed from mods folder and added to language/en/acp/search.php PHPBB3-10946 --- phpBB/includes/search/fulltext_sphinx.php | 6 --- phpBB/language/en/acp/search.php | 30 ++++++++++++++ phpBB/language/en/mods/fulltext_sphinx.php | 65 ------------------------------ 3 files changed, 30 insertions(+), 71 deletions(-) delete mode 100644 phpBB/language/en/mods/fulltext_sphinx.php (limited to 'phpBB') diff --git a/phpBB/includes/search/fulltext_sphinx.php b/phpBB/includes/search/fulltext_sphinx.php index fb16c5639b..2e263c1b55 100644 --- a/phpBB/includes/search/fulltext_sphinx.php +++ b/phpBB/includes/search/fulltext_sphinx.php @@ -733,8 +733,6 @@ class phpbb_search_fulltext_sphinx if (!isset($config['fulltext_sphinx_configured']) || !$config['fulltext_sphinx_configured']) { - $user->add_lang('mods/fulltext_sphinx'); - return $user->lang['FULLTEXT_SPHINX_CONFIGURE_FIRST']; } @@ -902,8 +900,6 @@ class phpbb_search_fulltext_sphinx $this->get_stats(); } - $user->add_lang('mods/fulltext_sphinx'); - return array( $user->lang['FULLTEXT_SPHINX_MAIN_POSTS'] => ($this->index_created()) ? $this->stats['main_posts'] : 0, $user->lang['FULLTEXT_SPHINX_DELTA_POSTS'] => ($this->index_created()) ? $this->stats['total_posts'] - $this->stats['main_posts'] : 0, @@ -966,8 +962,6 @@ class phpbb_search_fulltext_sphinx { global $user, $config; - $user->add_lang('mods/fulltext_sphinx'); - $config_vars = array( 'fulltext_sphinx_autoconf' => 'bool', 'fulltext_sphinx_autorun' => 'bool', diff --git a/phpBB/language/en/acp/search.php b/phpBB/language/en/acp/search.php index cd319c66a9..3fa7f34c64 100644 --- a/phpBB/language/en/acp/search.php +++ b/phpBB/language/en/acp/search.php @@ -69,6 +69,36 @@ $lang = array_merge($lang, array( 'FULLTEXT_POSTGRES_MIN_WORD_LEN_EXPLAIN' => 'Words with at least this many characters will be included in the query to the database.', 'FULLTEXT_POSTGRES_MAX_WORD_LEN_EXPLAIN' => 'Words with no more than this many characters will be included in the query to the database.', + 'FULLTEXT_SPHINX_AUTOCONF' => 'Automatically configure Sphinx', + 'FULLTEXT_SPHINX_AUTOCONF_EXPLAIN' => 'This is the easiest way to install Sphinx, just select the settings here and a config file will be written for you. This requires write permissions on the configuration folder.', + 'FULLTEXT_SPHINX_AUTORUN' => 'Automatically run Sphinx', + 'FULLTEXT_SPHINX_AUTORUN_EXPLAIN' => 'This is the easiest way to run Sphinx. Select the paths in this dialogue and the Sphinx daemon will be started and stopped as needed. You can also create an index from the ACP. If your PHP installation forbids the use of exec you can disable this and run Sphinx manually.', + 'FULLTEXT_SPHINX_BIN_PATH' => 'Path to executables directory', + 'FULLTEXT_SPHINX_BIN_PATH_EXPLAIN' => 'Skip if autorun is disabled. If this path could not be determined automatically you have to enter the path to the directory in which the sphinx executables indexer and searchd reside.', + 'FULLTEXT_SPHINX_CONFIG_PATH' => 'Path to configuration directory', + 'FULLTEXT_SPHINX_CONFIG_PATH_EXPLAIN' => 'Skip if autoconf is disabled. You should create this config directory outside the web accessable directories. It has to be writable by the user as which your webserver is running (often www-data or nobody).', + 'FULLTEXT_SPHINX_CONFIGURE_FIRST' => 'Before you create an index you have to enable and configure sphinx under GENERAL -> SERVER CONFIGURATION -> Search settings.', + 'FULLTEXT_SPHINX_CONFIGURE_BEFORE' => 'Configure the following settings BEFORE activating Sphinx', + 'FULLTEXT_SPHINX_CONFIGURE_AFTER' => 'The following settings do not have to be configured before activating Sphinx', + 'FULLTEXT_SPHINX_DATA_PATH' => 'Path to data directory', + 'FULLTEXT_SPHINX_DATA_PATH_EXPLAIN' => 'Skip if autorun is disabled. You should create this directory outside the web accessable directories. It has to be writable by the user as which your webserver is running (often www-data or nobody). It will be used to store the indexes and log files.', + 'FULLTEXT_SPHINX_DELTA_POSTS' => 'Number of posts in frequently updated delta index', + 'FULLTEXT_SPHINX_DIRECTORY_NOT_FOUND' => 'The directory %s does not exist. Please correct your path settings.', + 'FULLTEXT_SPHINX_FILE_NOT_EXECUTABLE' => 'The file %s is not executable for the webserver.', + 'FULLTEXT_SPHINX_FILE_NOT_FOUND' => 'The file %s does not exist. Please correct your path settings.', + 'FULLTEXT_SPHINX_FILE_NOT_WRITABLE' => 'The file %s cannot be written by the webserver.', + 'FULLTEXT_SPHINX_INDEXER_MEM_LIMIT' => 'Indexer memory limit', + 'FULLTEXT_SPHINX_INDEXER_MEM_LIMIT_EXPLAIN' => 'This number should at all times be lower than the RAM available on your machine. If you experience periodic performance problems this might be due to the indexer consuming too many resources. It might help to lower the amount of memory available to the indexer.', + 'FULLTEXT_SPHINX_LAST_SEARCHES' => 'Recent search queries', + 'FULLTEXT_SPHINX_MAIN_POSTS' => 'Number of posts in main index', + 'FULLTEXT_SPHINX_PORT' => 'Sphinx search deamon port', + 'FULLTEXT_SPHINX_PORT_EXPLAIN' => 'Port on which the sphinx search deamon on localhost listens. Leave empty to use the default 3312', + 'FULLTEXT_SPHINX_REQUIRES_EXEC' => 'The sphinx plugin for phpBB requires PHP’s exec function which is disabled on your system.', + 'FULLTEXT_SPHINX_UNCONFIGURED' => 'Please set all necessary options in the "Fulltext Sphinx" section of the previous page before you try to activate the sphinx plugin.', + 'FULLTEXT_SPHINX_WRONG_DATABASE' => 'The sphinx plugin for phpBB currently only supports MySQL', + 'FULLTEXT_SPHINX_STOPWORDS_FILE' => 'Stopwords activated', + 'FULLTEXT_SPHINX_STOPWORDS_FILE_EXPLAIN' => 'This setting only works with autoconf enabled. You can place a file called sphinx_stopwords.txt containing one word in each line in your config directory. If this file is present these words will be excluded from the indexing process.', + 'GENERAL_SEARCH_SETTINGS' => 'General search settings', 'GO_TO_SEARCH_INDEX' => 'Go to search index page', diff --git a/phpBB/language/en/mods/fulltext_sphinx.php b/phpBB/language/en/mods/fulltext_sphinx.php deleted file mode 100644 index f3fd68aa62..0000000000 --- a/phpBB/language/en/mods/fulltext_sphinx.php +++ /dev/null @@ -1,65 +0,0 @@ - 'Automatically configure Sphinx', - 'FULLTEXT_SPHINX_AUTOCONF_EXPLAIN' => 'This is the easiest way to install Sphinx, just select the settings here and a config file will be written for you. This requires write permissions on the configuration folder.', - 'FULLTEXT_SPHINX_AUTORUN' => 'Automatically run Sphinx', - 'FULLTEXT_SPHINX_AUTORUN_EXPLAIN' => 'This is the easiest way to run Sphinx. Select the paths in this dialogue and the Sphinx daemon will be started and stopped as needed. You can also create an index from the ACP. If your PHP installation forbids the use of exec you can disable this and run Sphinx manually.', - 'FULLTEXT_SPHINX_BIN_PATH' => 'Path to executables directory', - 'FULLTEXT_SPHINX_BIN_PATH_EXPLAIN' => 'Skip if autorun is disabled. If this path could not be determined automatically you have to enter the path to the directory in which the sphinx executables indexer and searchd reside.', - 'FULLTEXT_SPHINX_CONFIG_PATH' => 'Path to configuration directory', - 'FULLTEXT_SPHINX_CONFIG_PATH_EXPLAIN' => 'Skip if autoconf is disabled. You should create this config directory outside the web accessable directories. It has to be writable by the user as which your webserver is running (often www-data or nobody).', - 'FULLTEXT_SPHINX_CONFIGURE_FIRST' => 'Before you create an index you have to enable and configure sphinx under GENERAL -> SERVER CONFIGURATION -> Search settings.', - 'FULLTEXT_SPHINX_CONFIGURE_BEFORE' => 'Configure the following settings BEFORE activating Sphinx', - 'FULLTEXT_SPHINX_CONFIGURE_AFTER' => 'The following settings do not have to be configured before activating Sphinx', - 'FULLTEXT_SPHINX_DATA_PATH' => 'Path to data directory', - 'FULLTEXT_SPHINX_DATA_PATH_EXPLAIN' => 'Skip if autorun is disabled. You should create this directory outside the web accessable directories. It has to be writable by the user as which your webserver is running (often www-data or nobody). It will be used to store the indexes and log files.', - 'FULLTEXT_SPHINX_DELTA_POSTS' => 'Number of posts in frequently updated delta index', - 'FULLTEXT_SPHINX_DIRECTORY_NOT_FOUND' => 'The directory %s does not exist. Please correct your path settings.', - 'FULLTEXT_SPHINX_FILE_NOT_EXECUTABLE' => 'The file %s is not executable for the webserver.', - 'FULLTEXT_SPHINX_FILE_NOT_FOUND' => 'The file %s does not exist. Please correct your path settings.', - 'FULLTEXT_SPHINX_FILE_NOT_WRITABLE' => 'The file %s cannot be written by the webserver.', - 'FULLTEXT_SPHINX_INDEXER_MEM_LIMIT' => 'Indexer memory limit', - 'FULLTEXT_SPHINX_INDEXER_MEM_LIMIT_EXPLAIN' => 'This number should at all times be lower than the RAM available on your machine. If you experience periodic performance problems this might be due to the indexer consuming too many resources. It might help to lower the amount of memory available to the indexer.', - 'FULLTEXT_SPHINX_LAST_SEARCHES' => 'Recent search queries', - 'FULLTEXT_SPHINX_MAIN_POSTS' => 'Number of posts in main index', - 'FULLTEXT_SPHINX_PORT' => 'Sphinx search deamon port', - 'FULLTEXT_SPHINX_PORT_EXPLAIN' => 'Port on which the sphinx search deamon on localhost listens. Leave empty to use the default 3312', - 'FULLTEXT_SPHINX_REQUIRES_EXEC' => 'The sphinx plugin for phpBB requires PHP’s exec function which is disabled on your system.', - 'FULLTEXT_SPHINX_UNCONFIGURED' => 'Please set all necessary options in the "Fulltext Sphinx" section of the previous page before you try to activate the sphinx plugin.', - 'FULLTEXT_SPHINX_WRONG_DATABASE' => 'The sphinx plugin for phpBB currently only supports MySQL', - 'FULLTEXT_SPHINX_STOPWORDS_FILE' => 'Stopwords activated', - 'FULLTEXT_SPHINX_STOPWORDS_FILE_EXPLAIN' => 'This setting only works with autoconf enabled. You can place a file called sphinx_stopwords.txt containing one word in each line in your config directory. If this file is present these words will be excluded from the indexing process.', -)); - -?> -- cgit v1.2.1 From bfd01f01877bcb9a9be9e2df5c6713c3e338579e Mon Sep 17 00:00:00 2001 From: Dhruv Goel Date: Wed, 27 Jun 2012 01:03:04 +0530 Subject: [feature/sphinx-fulltext-search] remove all reference returns PHPBB3-10946 --- phpBB/includes/functions_sphinx.php | 8 ++++---- phpBB/includes/search/fulltext_sphinx.php | 10 +++++----- 2 files changed, 9 insertions(+), 9 deletions(-) (limited to 'phpBB') diff --git a/phpBB/includes/functions_sphinx.php b/phpBB/includes/functions_sphinx.php index a4f0e41491..a93a1d950f 100644 --- a/phpBB/includes/functions_sphinx.php +++ b/phpBB/includes/functions_sphinx.php @@ -44,7 +44,7 @@ class sphinx_config * @param string $name The name of the section that shall be returned * @return sphinx_config_section The section object or null if none was found */ - function &get_section_by_name($name) + function get_section_by_name($name) { for ($i = 0, $size = sizeof($this->sections); $i < $size; $i++) { @@ -62,7 +62,7 @@ class sphinx_config * @param string $name The name for the new section * @return sphinx_config_section The newly created section object */ - function &add_section($name) + function add_section($name) { $this->sections[] = new sphinx_config_section($name, ''); return $this->sections[sizeof($this->sections) - 1]; @@ -345,7 +345,7 @@ class sphinx_config_section * @return sphinx_config_section The first variable object from this section with the * given name or null if none was found */ - function &get_variable_by_name($name) + function get_variable_by_name($name) { for ($i = 0, $size = sizeof($this->variables); $i < $size; $i++) { @@ -382,7 +382,7 @@ class sphinx_config_section * @param string $value The value for the new variable * @return sphinx_config_variable Variable object that was created */ - function &create_variable($name, $value) + function create_variable($name, $value) { $this->variables[] = new sphinx_config_variable($name, $value, ''); return $this->variables[sizeof($this->variables) - 1]; diff --git a/phpBB/includes/search/fulltext_sphinx.php b/phpBB/includes/search/fulltext_sphinx.php index 2e263c1b55..477b1646fb 100644 --- a/phpBB/includes/search/fulltext_sphinx.php +++ b/phpBB/includes/search/fulltext_sphinx.php @@ -323,10 +323,10 @@ class phpbb_search_fulltext_sphinx foreach ($config_data as $section_name => $section_data) { - $section = &$config_object->get_section_by_name($section_name); + $section = $config_object->get_section_by_name($section_name); if (!$section) { - $section = &$config_object->add_section($section_name); + $section = $config_object->add_section($section_name); } foreach ($delete as $key => $void) @@ -346,10 +346,10 @@ class phpbb_search_fulltext_sphinx if (!isset($non_unique[$key])) { - $variable = &$section->get_variable_by_name($key); + $variable = $section->get_variable_by_name($key); if (!$variable) { - $variable = &$section->create_variable($key, $value); + $variable = $section->create_variable($key, $value); } else { @@ -358,7 +358,7 @@ class phpbb_search_fulltext_sphinx } else { - $variable = &$section->create_variable($key, $value); + $variable = $section->create_variable($key, $value); } } } -- cgit v1.2.1 From 39f8a5fa9f71724d0abd98cdf7a7d82fc7e7bb0f Mon Sep 17 00:00:00 2001 From: Dhruv Goel Date: Wed, 27 Jun 2012 03:44:03 +0530 Subject: [feature/sphinx-fulltext-search] use sql_build_query for query Uses sql_build_query for JOIN query. Remove casting to int and space for phpbb conventions to be followed PHPBB3-10946 Conflicts: phpBB/includes/search/fulltext_sphinx.php --- phpBB/includes/search/fulltext_sphinx.php | 24 +++++++++++++++++------- 1 file changed, 17 insertions(+), 7 deletions(-) (limited to 'phpBB') diff --git a/phpBB/includes/search/fulltext_sphinx.php b/phpBB/includes/search/fulltext_sphinx.php index 477b1646fb..2690612b1a 100644 --- a/phpBB/includes/search/fulltext_sphinx.php +++ b/phpBB/includes/search/fulltext_sphinx.php @@ -53,7 +53,7 @@ class phpbb_search_fulltext_sphinx $this->id = $config['avatar_salt']; $this->indexes = 'index_phpbb_' . $this->id . '_delta;index_phpbb_' . $this->id . '_main'; - $this->sphinx = new SphinxClient (); + $this->sphinx = new SphinxClient(); if (!empty($config['fulltext_sphinx_configured'])) { @@ -648,18 +648,28 @@ class phpbb_search_fulltext_sphinx } else if ($mode != 'post' && $post_id) { - // update topic_last_post_time for full topic - $sql = 'SELECT p1.post_id - FROM ' . POSTS_TABLE . ' p1 - LEFT JOIN ' . POSTS_TABLE . ' p2 ON (p1.topic_id = p2.topic_id) - WHERE p2.post_id = ' . $post_id; + // Update topic_last_post_time for full topic + $sql_array = array( + 'SELECT' => 'p1.post_id', + 'FROM' => array( + POSTS_TABLE => 'p1', + ), + 'LEFT_JOIN' => array(array( + 'FROM' => array( + POSTS_TABLE => 'p2' + ), + 'ON' => 'p1.topic_id = p2.topic_id', + )), + ); + + $sql = $db->sql_build_query('SELECT', $sql_array); $result = $db->sql_query($sql); $post_updates = array(); $post_time = time(); while ($row = $db->sql_fetchrow($result)) { - $post_updates[(int)$row['post_id']] = array((int) $post_time); + $post_updates[(int)$row['post_id']] = array($post_time); } $db->sql_freeresult($result); -- cgit v1.2.1 From 10b706674e0fc100ff4e21d5fe100a9b532bb4bf Mon Sep 17 00:00:00 2001 From: Dhruv Goel Date: Tue, 10 Jul 2012 01:49:38 +0530 Subject: [feature/sphinx-fulltext-search] add binlog_path to config binlog files are now added to the data folder. PHPBB3-10946 --- phpBB/docs/sphinx.sample.conf | 1 + phpBB/includes/search/fulltext_sphinx.php | 1 + 2 files changed, 2 insertions(+) (limited to 'phpBB') diff --git a/phpBB/docs/sphinx.sample.conf b/phpBB/docs/sphinx.sample.conf index d7e59a11fc..000d8157d6 100644 --- a/phpBB/docs/sphinx.sample.conf +++ b/phpBB/docs/sphinx.sample.conf @@ -93,4 +93,5 @@ searchd max_children = 30 pid_file = {DATA_PATH}/searchd.pid max_matches = 20000 + binlog_path = {DATA_PATH} } diff --git a/phpBB/includes/search/fulltext_sphinx.php b/phpBB/includes/search/fulltext_sphinx.php index 2690612b1a..8514b9cabb 100644 --- a/phpBB/includes/search/fulltext_sphinx.php +++ b/phpBB/includes/search/fulltext_sphinx.php @@ -315,6 +315,7 @@ class phpbb_search_fulltext_sphinx array('max_children', '30'), array('pid_file', $config['fulltext_sphinx_data_path'] . "searchd.pid"), array('max_matches', (string) MAX_MATCHES), + array('binlog_path', $config['fulltext_sphinx_data_path']), ), ); -- cgit v1.2.1 From 97fda78e7d85444f21ba2b10b3d58c4639b85936 Mon Sep 17 00:00:00 2001 From: Dhruv Goel Date: Sun, 1 Jul 2012 01:23:57 +0530 Subject: [feature/sphinx-fulltext-search] Make different files for different classes Break the classes in functions-sphinx.php into different files with proper class names according to phpbb class auto loader conventions. PHPBB3-10946 Conflicts: phpBB/includes/search/sphinx/config.php --- phpBB/includes/functions_sphinx.php | 502 ----------------------- phpBB/includes/search/fulltext_sphinx.php | 7 +- phpBB/includes/search/sphinx/config.php | 287 +++++++++++++ phpBB/includes/search/sphinx/config_comment.php | 45 ++ phpBB/includes/search/sphinx/config_section.php | 144 +++++++ phpBB/includes/search/sphinx/config_variable.php | 72 ++++ 6 files changed, 549 insertions(+), 508 deletions(-) delete mode 100644 phpBB/includes/functions_sphinx.php create mode 100644 phpBB/includes/search/sphinx/config.php create mode 100644 phpBB/includes/search/sphinx/config_comment.php create mode 100644 phpBB/includes/search/sphinx/config_section.php create mode 100644 phpBB/includes/search/sphinx/config_variable.php (limited to 'phpBB') diff --git a/phpBB/includes/functions_sphinx.php b/phpBB/includes/functions_sphinx.php deleted file mode 100644 index a93a1d950f..0000000000 --- a/phpBB/includes/functions_sphinx.php +++ /dev/null @@ -1,502 +0,0 @@ -read($filename); - } - } - - /** - * Get a section object by its name - * - * @param string $name The name of the section that shall be returned - * @return sphinx_config_section The section object or null if none was found - */ - function get_section_by_name($name) - { - for ($i = 0, $size = sizeof($this->sections); $i < $size; $i++) - { - // make sure this is really a section object and not a comment - if (($this->sections[$i] instanceof sphinx_config_section) && $this->sections[$i]->get_name() == $name) - { - return $this->sections[$i]; - } - } - } - - /** - * Appends a new empty section to the end of the config - * - * @param string $name The name for the new section - * @return sphinx_config_section The newly created section object - */ - function add_section($name) - { - $this->sections[] = new sphinx_config_section($name, ''); - return $this->sections[sizeof($this->sections) - 1]; - } - - /** - * Parses the config file at the given path, which is stored in $this->loaded for later use - * - * @param string $filename The path to the config file - */ - function read($filename) - { - // split the file into lines, we'll process it line by line - $config_file = file($filename); - - $this->sections = array(); - - $section = null; - $found_opening_bracket = false; - $in_value = false; - - foreach ($config_file as $i => $line) - { - // if the value of a variable continues to the next line because the line break was escaped - // then we don't trim leading space but treat it as a part of the value - if ($in_value) - { - $line = rtrim($line); - } - else - { - $line = trim($line); - } - - // if we're not inside a section look for one - if (!$section) - { - // add empty lines and comments as comment objects to the section list - // that way they're not deleted when reassembling the file from the sections - if (!$line || $line[0] == '#') - { - $this->sections[] = new sphinx_config_comment($config_file[$i]); - continue; - } - else - { - // otherwise we scan the line reading the section name until we find - // an opening curly bracket or a comment - $section_name = ''; - $section_name_comment = ''; - $found_opening_bracket = false; - for ($j = 0, $length = strlen($line); $j < $length; $j++) - { - if ($line[$j] == '#') - { - $section_name_comment = substr($line, $j); - break; - } - - if ($found_opening_bracket) - { - continue; - } - - if ($line[$j] == '{') - { - $found_opening_bracket = true; - continue; - } - - $section_name .= $line[$j]; - } - - // and then we create the new section object - $section_name = trim($section_name); - $section = new sphinx_config_section($section_name, $section_name_comment); - } - } - else // if we're looking for variables inside a section - { - $skip_first = false; - - // if we're not in a value continuing over the line feed - if (!$in_value) - { - // then add empty lines and comments as comment objects to the variable list - // of this section so they're not deleted on reassembly - if (!$line || $line[0] == '#') - { - $section->add_variable(new sphinx_config_comment($config_file[$i])); - continue; - } - - // as long as we haven't yet actually found an opening bracket for this section - // we treat everything as comments so it's not deleted either - if (!$found_opening_bracket) - { - if ($line[0] == '{') - { - $skip_first = true; - $line = substr($line, 1); - $found_opening_bracket = true; - } - else - { - $section->add_variable(new sphinx_config_comment($config_file[$i])); - continue; - } - } - } - - // if we did not find a comment in this line or still add to the previous line's value ... - if ($line || $in_value) - { - if (!$in_value) - { - $name = ''; - $value = ''; - $comment = ''; - $found_assignment = false; - } - $in_value = false; - $end_section = false; - - /* ... then we should prase this line char by char: - - first there's the variable name - - then an equal sign - - the variable value - - possibly a backslash before the linefeed in this case we need to continue - parsing the value in the next line - - a # indicating that the rest of the line is a comment - - a closing curly bracket indicating the end of this section*/ - for ($j = 0, $length = strlen($line); $j < $length; $j++) - { - if ($line[$j] == '#') - { - $comment = substr($line, $j); - break; - } - else if ($line[$j] == '}') - { - $comment = substr($line, $j + 1); - $end_section = true; - break; - } - else if (!$found_assignment) - { - if ($line[$j] == '=') - { - $found_assignment = true; - } - else - { - $name .= $line[$j]; - } - } - else - { - if ($line[$j] == '\\' && $j == $length - 1) - { - $value .= "\n"; - $in_value = true; - continue 2; // go to the next line and keep processing the value in there - } - $value .= $line[$j]; - } - } - - // if a name and an equal sign were found then we have append a new variable object to the section - if ($name && $found_assignment) - { - $section->add_variable(new sphinx_config_variable(trim($name), trim($value), ($end_section) ? '' : $comment)); - continue; - } - - // if we found a closing curly bracket this section has been completed and we can append it to the section list - // and continue with looking for the next section - if ($end_section) - { - $section->set_end_comment($comment); - $this->sections[] = $section; - $section = null; - continue; - } - } - - // if we did not find anything meaningful up to here, then just treat it as a comment - $comment = ($skip_first) ? "\t" . substr(ltrim($config_file[$i]), 1) : $config_file[$i]; - $section->add_variable(new sphinx_config_comment($comment)); - } - } - - // keep the filename for later use - $this->loaded = $filename; - } - - /** - * Writes the config data into a file - * - * @param string $filename The optional filename into which the config data shall be written. - * If it's not specified it will be written into the file that the config - * was originally read from. - */ - function write($filename = false) - { - if ($filename === false && $this->loaded) - { - $filename = $this->loaded; - } - - $data = ""; - foreach ($this->sections as $section) - { - $data .= $section->to_string(); - } - - $fp = fopen($filename, 'wb'); - fwrite($fp, $data); - fclose($fp); - } -} - -/** -* sphinx_config_section -* Represents a single section inside the sphinx configuration -*/ -class sphinx_config_section -{ - var $name; - var $comment; - var $end_comment; - var $variables = array(); - - /** - * Construct a new section - * - * @param string $name Name of the section - * @param string $comment Comment that should be appended after the name in the - * textual format. - */ - function sphinx_config_section($name, $comment) - { - $this->name = $name; - $this->comment = $comment; - $this->end_comment = ''; - } - - /** - * Add a variable object to the list of variables in this section - * - * @param sphinx_config_variable $variable The variable object - */ - function add_variable($variable) - { - $this->variables[] = $variable; - } - - /** - * Adds a comment after the closing bracket in the textual representation - */ - function set_end_comment($end_comment) - { - $this->end_comment = $end_comment; - } - - /** - * Getter for the name of this section - * - * @return string Section's name - */ - function get_name() - { - return $this->name; - } - - /** - * Get a variable object by its name - * - * @param string $name The name of the variable that shall be returned - * @return sphinx_config_section The first variable object from this section with the - * given name or null if none was found - */ - function get_variable_by_name($name) - { - for ($i = 0, $size = sizeof($this->variables); $i < $size; $i++) - { - // make sure this is a variable object and not a comment - if (($this->variables[$i] instanceof sphinx_config_variable) && $this->variables[$i]->get_name() == $name) - { - return $this->variables[$i]; - } - } - } - - /** - * Deletes all variables with the given name - * - * @param string $name The name of the variable objects that are supposed to be removed - */ - function delete_variables_by_name($name) - { - for ($i = 0, $size = sizeof($this->variables); $i < $size; $i++) - { - // make sure this is a variable object and not a comment - if (($this->variables[$i] instanceof sphinx_config_variable) && $this->variables[$i]->get_name() == $name) - { - array_splice($this->variables, $i, 1); - $i--; - } - } - } - - /** - * Create a new variable object and append it to the variable list of this section - * - * @param string $name The name for the new variable - * @param string $value The value for the new variable - * @return sphinx_config_variable Variable object that was created - */ - function create_variable($name, $value) - { - $this->variables[] = new sphinx_config_variable($name, $value, ''); - return $this->variables[sizeof($this->variables) - 1]; - } - - /** - * Turns this object into a string which can be written to a config file - * - * @return string Config data in textual form, parsable for sphinx - */ - function to_string() - { - $content = $this->name . " " . $this->comment . "\n{\n"; - - // make sure we don't get too many newlines after the opening bracket - while (trim($this->variables[0]->to_string()) == "") - { - array_shift($this->variables); - } - - foreach ($this->variables as $variable) - { - $content .= $variable->to_string(); - } - $content .= '}' . $this->end_comment . "\n"; - - return $content; - } -} - -/** -* sphinx_config_variable -* Represents a single variable inside the sphinx configuration -*/ -class sphinx_config_variable -{ - var $name; - var $value; - var $comment; - - /** - * Constructs a new variable object - * - * @param string $name Name of the variable - * @param string $value Value of the variable - * @param string $comment Optional comment after the variable in the - * config file - */ - function sphinx_config_variable($name, $value, $comment) - { - $this->name = $name; - $this->value = $value; - $this->comment = $comment; - } - - /** - * Getter for the variable's name - * - * @return string The variable object's name - */ - function get_name() - { - return $this->name; - } - - /** - * Allows changing the variable's value - * - * @param string $value New value for this variable - */ - function set_value($value) - { - $this->value = $value; - } - - /** - * Turns this object into a string readable by sphinx - * - * @return string Config data in textual form - */ - function to_string() - { - return "\t" . $this->name . ' = ' . str_replace("\n", "\\\n", $this->value) . ' ' . $this->comment . "\n"; - } -} - - -/** -* sphinx_config_comment -* Represents a comment inside the sphinx configuration -*/ -class sphinx_config_comment -{ - var $exact_string; - - /** - * Create a new comment - * - * @param string $exact_string The content of the comment including newlines, leading whitespace, etc. - */ - function sphinx_config_comment($exact_string) - { - $this->exact_string = $exact_string; - } - - /** - * Simply returns the comment as it was created - * - * @return string The exact string that was specified in the constructor - */ - function to_string() - { - return $this->exact_string; - } -} - -?> diff --git a/phpBB/includes/search/fulltext_sphinx.php b/phpBB/includes/search/fulltext_sphinx.php index 8514b9cabb..6f3c688aed 100644 --- a/phpBB/includes/search/fulltext_sphinx.php +++ b/phpBB/includes/search/fulltext_sphinx.php @@ -207,11 +207,6 @@ class phpbb_search_fulltext_sphinx // now that we're sure everything was entered correctly, generate a config for the index // we misuse the avatar_salt for this, as it should be unique ;-) - if (!class_exists('sphinx_config')) - { - include($phpbb_root_path . 'includes/functions_sphinx.php'); - } - if (!file_exists($config['fulltext_sphinx_config_path'] . 'sphinx.conf')) { $filename = $config['fulltext_sphinx_config_path'] . 'sphinx.conf'; @@ -223,7 +218,7 @@ class phpbb_search_fulltext_sphinx @fclose($fp); } - $config_object = new sphinx_config($config['fulltext_sphinx_config_path'] . 'sphinx.conf'); + $config_object = new phpbb_search_sphinx_config($config['fulltext_sphinx_config_path'] . 'sphinx.conf'); $config_data = array( 'source source_phpbb_' . $this->id . '_main' => array( diff --git a/phpBB/includes/search/sphinx/config.php b/phpBB/includes/search/sphinx/config.php new file mode 100644 index 0000000000..966cd0f284 --- /dev/null +++ b/phpBB/includes/search/sphinx/config.php @@ -0,0 +1,287 @@ +read($filename); + } + } + + /** + * Get a section object by its name + * + * @param string $name The name of the section that shall be returned + * @return phpbb_search_sphinx_config_section The section object or null if none was found + */ + function get_section_by_name($name) + { + for ($i = 0, $size = sizeof($this->sections); $i < $size; $i++) + { + // make sure this is really a section object and not a comment + if (($this->sections[$i] instanceof phpbb_search_sphinx_config_section) && $this->sections[$i]->get_name() == $name) + { + return $this->sections[$i]; + } + } + } + + /** + * Appends a new empty section to the end of the config + * + * @param string $name The name for the new section + * @return phpbb_search_sphinx_config_section The newly created section object + */ + function add_section($name) + { + $this->sections[] = new phpbb_search_sphinx_config_section($name, ''); + return $this->sections[sizeof($this->sections) - 1]; + } + + /** + * Parses the config file at the given path, which is stored in $this->loaded for later use + * + * @param string $filename The path to the config file + */ + function read($filename) + { + // split the file into lines, we'll process it line by line + $config_file = file($filename); + + $this->sections = array(); + + $section = null; + $found_opening_bracket = false; + $in_value = false; + + foreach ($config_file as $i => $line) + { + // if the value of a variable continues to the next line because the line break was escaped + // then we don't trim leading space but treat it as a part of the value + if ($in_value) + { + $line = rtrim($line); + } + else + { + $line = trim($line); + } + + // if we're not inside a section look for one + if (!$section) + { + // add empty lines and comments as comment objects to the section list + // that way they're not deleted when reassembling the file from the sections + if (!$line || $line[0] == '#') + { + $this->sections[] = new phpbb_search_sphinx_config_comment($config_file[$i]); + continue; + } + else + { + // otherwise we scan the line reading the section name until we find + // an opening curly bracket or a comment + $section_name = ''; + $section_name_comment = ''; + $found_opening_bracket = false; + for ($j = 0, $length = strlen($line); $j < $length; $j++) + { + if ($line[$j] == '#') + { + $section_name_comment = substr($line, $j); + break; + } + + if ($found_opening_bracket) + { + continue; + } + + if ($line[$j] == '{') + { + $found_opening_bracket = true; + continue; + } + + $section_name .= $line[$j]; + } + + // and then we create the new section object + $section_name = trim($section_name); + $section = new phpbb_search_sphinx_config_section($section_name, $section_name_comment); + } + } + else // if we're looking for variables inside a section + { + $skip_first = false; + + // if we're not in a value continuing over the line feed + if (!$in_value) + { + // then add empty lines and comments as comment objects to the variable list + // of this section so they're not deleted on reassembly + if (!$line || $line[0] == '#') + { + $section->add_variable(new phpbb_search_sphinx_config_comment($config_file[$i])); + continue; + } + + // as long as we haven't yet actually found an opening bracket for this section + // we treat everything as comments so it's not deleted either + if (!$found_opening_bracket) + { + if ($line[0] == '{') + { + $skip_first = true; + $line = substr($line, 1); + $found_opening_bracket = true; + } + else + { + $section->add_variable(new phpbb_search_sphinx_config_comment($config_file[$i])); + continue; + } + } + } + + // if we did not find a comment in this line or still add to the previous line's value ... + if ($line || $in_value) + { + if (!$in_value) + { + $name = ''; + $value = ''; + $comment = ''; + $found_assignment = false; + } + $in_value = false; + $end_section = false; + + /* ... then we should prase this line char by char: + - first there's the variable name + - then an equal sign + - the variable value + - possibly a backslash before the linefeed in this case we need to continue + parsing the value in the next line + - a # indicating that the rest of the line is a comment + - a closing curly bracket indicating the end of this section*/ + for ($j = 0, $length = strlen($line); $j < $length; $j++) + { + if ($line[$j] == '#') + { + $comment = substr($line, $j); + break; + } + else if ($line[$j] == '}') + { + $comment = substr($line, $j + 1); + $end_section = true; + break; + } + else if (!$found_assignment) + { + if ($line[$j] == '=') + { + $found_assignment = true; + } + else + { + $name .= $line[$j]; + } + } + else + { + if ($line[$j] == '\\' && $j == $length - 1) + { + $value .= "\n"; + $in_value = true; + continue 2; // go to the next line and keep processing the value in there + } + $value .= $line[$j]; + } + } + + // if a name and an equal sign were found then we have append a new variable object to the section + if ($name && $found_assignment) + { + $section->add_variable(new phpbb_search_sphinx_config_variable(trim($name), trim($value), ($end_section) ? '' : $comment)); + continue; + } + + // if we found a closing curly bracket this section has been completed and we can append it to the section list + // and continue with looking for the next section + if ($end_section) + { + $section->set_end_comment($comment); + $this->sections[] = $section; + $section = null; + continue; + } + } + + // if we did not find anything meaningful up to here, then just treat it as a comment + $comment = ($skip_first) ? "\t" . substr(ltrim($config_file[$i]), 1) : $config_file[$i]; + $section->add_variable(new phpbb_search_sphinx_config_comment($comment)); + } + } + + // keep the filename for later use + $this->loaded = $filename; + } + + /** + * Writes the config data into a file + * + * @param string $filename The optional filename into which the config data shall be written. + * If it's not specified it will be written into the file that the config + * was originally read from. + */ + function write($filename = false) + { + if ($filename === false && $this->loaded) + { + $filename = $this->loaded; + } + + $data = ""; + foreach ($this->sections as $section) + { + $data .= $section->to_string(); + } + + $fp = fopen($filename, 'wb'); + fwrite($fp, $data); + fclose($fp); + } +} diff --git a/phpBB/includes/search/sphinx/config_comment.php b/phpBB/includes/search/sphinx/config_comment.php new file mode 100644 index 0000000000..63d3488aef --- /dev/null +++ b/phpBB/includes/search/sphinx/config_comment.php @@ -0,0 +1,45 @@ +exact_string = $exact_string; + } + + /** + * Simply returns the comment as it was created + * + * @return string The exact string that was specified in the constructor + */ + function to_string() + { + return $this->exact_string; + } +} diff --git a/phpBB/includes/search/sphinx/config_section.php b/phpBB/includes/search/sphinx/config_section.php new file mode 100644 index 0000000000..529254dd5a --- /dev/null +++ b/phpBB/includes/search/sphinx/config_section.php @@ -0,0 +1,144 @@ +name = $name; + $this->comment = $comment; + $this->end_comment = ''; + } + + /** + * Add a variable object to the list of variables in this section + * + * @param phpbb_search_sphinx_config_variable $variable The variable object + */ + function add_variable($variable) + { + $this->variables[] = $variable; + } + + /** + * Adds a comment after the closing bracket in the textual representation + */ + function set_end_comment($end_comment) + { + $this->end_comment = $end_comment; + } + + /** + * Getter for the name of this section + * + * @return string Section's name + */ + function get_name() + { + return $this->name; + } + + /** + * Get a variable object by its name + * + * @param string $name The name of the variable that shall be returned + * @return phpbb_search_sphinx_config_section The first variable object from this section with the + * given name or null if none was found + */ + function get_variable_by_name($name) + { + for ($i = 0, $size = sizeof($this->variables); $i < $size; $i++) + { + // make sure this is a variable object and not a comment + if (($this->variables[$i] instanceof phpbb_search_sphinx_config_variable) && $this->variables[$i]->get_name() == $name) + { + return $this->variables[$i]; + } + } + } + + /** + * Deletes all variables with the given name + * + * @param string $name The name of the variable objects that are supposed to be removed + */ + function delete_variables_by_name($name) + { + for ($i = 0, $size = sizeof($this->variables); $i < $size; $i++) + { + // make sure this is a variable object and not a comment + if (($this->variables[$i] instanceof phpbb_search_sphinx_config_variable) && $this->variables[$i]->get_name() == $name) + { + array_splice($this->variables, $i, 1); + $i--; + } + } + } + + /** + * Create a new variable object and append it to the variable list of this section + * + * @param string $name The name for the new variable + * @param string $value The value for the new variable + * @return phpbb_search_sphinx_config_variable Variable object that was created + */ + function create_variable($name, $value) + { + $this->variables[] = new phpbb_search_sphinx_config_variable($name, $value, ''); + return $this->variables[sizeof($this->variables) - 1]; + } + + /** + * Turns this object into a string which can be written to a config file + * + * @return string Config data in textual form, parsable for sphinx + */ + function to_string() + { + $content = $this->name . ' ' . $this->comment . "\n{\n"; + + // make sure we don't get too many newlines after the opening bracket + while (trim($this->variables[0]->to_string()) == '') + { + array_shift($this->variables); + } + + foreach ($this->variables as $variable) + { + $content .= $variable->to_string(); + } + $content .= '}' . $this->end_comment . "\n"; + + return $content; + } +} diff --git a/phpBB/includes/search/sphinx/config_variable.php b/phpBB/includes/search/sphinx/config_variable.php new file mode 100644 index 0000000000..dd7836f7c8 --- /dev/null +++ b/phpBB/includes/search/sphinx/config_variable.php @@ -0,0 +1,72 @@ +name = $name; + $this->value = $value; + $this->comment = $comment; + } + + /** + * Getter for the variable's name + * + * @return string The variable object's name + */ + function get_name() + { + return $this->name; + } + + /** + * Allows changing the variable's value + * + * @param string $value New value for this variable + */ + function set_value($value) + { + $this->value = $value; + } + + /** + * Turns this object into a string readable by sphinx + * + * @return string Config data in textual form + */ + function to_string() + { + return "\t" . $this->name . ' = ' . str_replace("\n", "\\\n", $this->value) . ' ' . $this->comment . "\n"; + } +} -- cgit v1.2.1 From 4a11a7b97027743b8239d31a4d51824bd807c5ac Mon Sep 17 00:00:00 2001 From: Dhruv Goel Date: Sun, 1 Jul 2012 03:03:15 +0530 Subject: [feature/sphinx-fulltext-search] add sphinx_table constant to constants.php PHPBB3-10946 --- phpBB/includes/constants.php | 1 + phpBB/includes/search/fulltext_sphinx.php | 1 - 2 files changed, 1 insertion(+), 1 deletion(-) (limited to 'phpBB') diff --git a/phpBB/includes/constants.php b/phpBB/includes/constants.php index 66d2a003c6..68af41ab20 100644 --- a/phpBB/includes/constants.php +++ b/phpBB/includes/constants.php @@ -260,6 +260,7 @@ define('SESSIONS_TABLE', $table_prefix . 'sessions'); define('SESSIONS_KEYS_TABLE', $table_prefix . 'sessions_keys'); define('SITELIST_TABLE', $table_prefix . 'sitelist'); define('SMILIES_TABLE', $table_prefix . 'smilies'); +define('SPHINX_TABLE', $table_prefix . 'sphinx'); define('STYLES_TABLE', $table_prefix . 'styles'); define('STYLES_TEMPLATE_TABLE', $table_prefix . 'styles_template'); define('STYLES_TEMPLATE_DATA_TABLE',$table_prefix . 'styles_template_data'); diff --git a/phpBB/includes/search/fulltext_sphinx.php b/phpBB/includes/search/fulltext_sphinx.php index 6f3c688aed..53bff898eb 100644 --- a/phpBB/includes/search/fulltext_sphinx.php +++ b/phpBB/includes/search/fulltext_sphinx.php @@ -26,7 +26,6 @@ require($phpbb_root_path . "includes/sphinxapi-0.9.8." . $phpEx); define('INDEXER_NAME', 'indexer'); define('SEARCHD_NAME', 'searchd'); -define('SPHINX_TABLE', $table_prefix . 'sphinx'); define('MAX_MATCHES', 20000); define('CONNECT_RETRIES', 3); -- cgit v1.2.1 From 06eeed058df75c41496c5306bfa35725c45cf5f3 Mon Sep 17 00:00:00 2001 From: Dhruv Goel Date: Sun, 1 Jul 2012 03:17:45 +0530 Subject: [feature/sphinx-fulltext-search] remove unused arrays PHPBB3-10946 --- phpBB/includes/search/fulltext_sphinx.php | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) (limited to 'phpBB') diff --git a/phpBB/includes/search/fulltext_sphinx.php b/phpBB/includes/search/fulltext_sphinx.php index 53bff898eb..4c0adcd99e 100644 --- a/phpBB/includes/search/fulltext_sphinx.php +++ b/phpBB/includes/search/fulltext_sphinx.php @@ -395,14 +395,7 @@ class phpbb_search_fulltext_sphinx $this->sphinx->SetMatchMode(SPH_MATCH_ANY); } - $match = array(); - // Keep quotes - $match[] = "#"#"; - // KeepNew lines - $match[] = "#[\n]+#"; - - $replace = array('"', " "); - + // Keep quotes and new lines $keywords = str_replace(array('"', "\n"), array('"', ' '), trim($keywords)); if (strlen($keywords) > 0) -- cgit v1.2.1 From 0e9174d168a82bde16ec59d615e19b85a50cebcf Mon Sep 17 00:00:00 2001 From: Dhruv Goel Date: Sun, 1 Jul 2012 05:06:51 +0530 Subject: [feature/sphinx-fulltext-search] use keywords_search instead of get_name using keyword_search method instead of get_name to distinguish between the search backend classes present in includes/search and other helper classes. PHPBB3-10946 --- phpBB/includes/acp/acp_search.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'phpBB') diff --git a/phpBB/includes/acp/acp_search.php b/phpBB/includes/acp/acp_search.php index 54a3e7aaa1..82d9b021fe 100644 --- a/phpBB/includes/acp/acp_search.php +++ b/phpBB/includes/acp/acp_search.php @@ -598,7 +598,7 @@ class acp_search { global $phpbb_root_path, $phpEx, $user; - if (!class_exists($type) || !method_exists($type, 'get_name')) + if (!class_exists($type) || !method_exists($type, 'keyword_search')) { $error = $user->lang['NO_SUCH_SEARCH_MODULE']; return $error; -- cgit v1.2.1 From 2503581cd562b39a108821da85cc0175735e24a5 Mon Sep 17 00:00:00 2001 From: Dhruv Goel Date: Tue, 10 Jul 2012 02:33:02 +0530 Subject: [feature/sphinx-fulltext-search] add class properties indexes & sphinx PHPBB3-10946 --- phpBB/includes/search/fulltext_sphinx.php | 2 ++ 1 file changed, 2 insertions(+) (limited to 'phpBB') diff --git a/phpBB/includes/search/fulltext_sphinx.php b/phpBB/includes/search/fulltext_sphinx.php index 4c0adcd99e..4ace7c9753 100644 --- a/phpBB/includes/search/fulltext_sphinx.php +++ b/phpBB/includes/search/fulltext_sphinx.php @@ -44,6 +44,8 @@ class phpbb_search_fulltext_sphinx var $search_query; var $common_words = array(); var $id; + var $indexes; + var $sphinx; public function __construct(&$error) { -- cgit v1.2.1 From 8dcdf8a9732a570f26fee802af4bfcbd25f16ec2 Mon Sep 17 00:00:00 2001 From: Dhruv Goel Date: Sun, 1 Jul 2012 12:01:14 +0530 Subject: [feature/sphinx-fulltext-search] add docblock and access modifiers PHPBB3-10946 Conflicts: phpBB/includes/search/fulltext_sphinx.php --- phpBB/includes/search/fulltext_sphinx.php | 85 +++++++++++++++++++++++++------ 1 file changed, 70 insertions(+), 15 deletions(-) (limited to 'phpBB') diff --git a/phpBB/includes/search/fulltext_sphinx.php b/phpBB/includes/search/fulltext_sphinx.php index 4ace7c9753..82addca28a 100644 --- a/phpBB/includes/search/fulltext_sphinx.php +++ b/phpBB/includes/search/fulltext_sphinx.php @@ -38,15 +38,21 @@ define('CONNECT_WAIT_TIME', 300); */ class phpbb_search_fulltext_sphinx { - var $stats = array(); - var $word_length = array(); - var $split_words = array(); - var $search_query; - var $common_words = array(); - var $id; - var $indexes; - var $sphinx; + private $stats = array(); + private $split_words = array(); + private $id; + private $indexes; + private $sphinx; + public $word_length = array(); + public $search_query; + public $common_words = array(); + /** + * Constructor + * Creates a new phpbb_search_fulltext_postgres, which is used as a search backend. + * + * @param string|bool $error Any error that occurs is passed on through this reference variable otherwise false + */ public function __construct(&$error) { global $config; @@ -81,6 +87,8 @@ class phpbb_search_fulltext_sphinx * Returns the name of this search backend to be displayed to administrators * * @return string Name + * + * @access public */ public function get_name() { @@ -89,6 +97,10 @@ class phpbb_search_fulltext_sphinx /** * Checks permissions and paths, if everything is correct it generates the config file + * + * @return string|bool Language key of the error/incompatiblity occured + * + * @access public */ function init() { @@ -110,6 +122,13 @@ class phpbb_search_fulltext_sphinx return false; } + /** + * Updates the config file sphinx.conf and generates the same in case autoconf is selected + * + * @return string|bool Language key of the error/incompatiblity occured otherwise false + * + * @access private + */ function config_updated() { global $db, $user, $config, $phpbb_root_path, $phpEx; @@ -378,6 +397,8 @@ class phpbb_search_fulltext_sphinx * @param string $keywords Contains the keyword as entered by the user * @param string $terms is either 'all' or 'any' * @return false if no valid keywords were found and otherwise true + * + * @access public */ function split_keywords(&$keywords, $terms) { @@ -619,14 +640,14 @@ class phpbb_search_fulltext_sphinx /** * Updates wordlist and wordmatch tables when a message is posted or changed * - * @param string $mode Contains the post mode: edit, post, reply, quote - * @param int $post_id The id of the post which is modified/created - * @param string &$message New or updated post content - * @param string &$subject New or updated post subject - * @param int $poster_id Post author's user id - * @param int $forum_id The id of the forum in which the post is located + * @param string $mode Contains the post mode: edit, post, reply, quote + * @param int $post_id The id of the post which is modified/created + * @param string &$message New or updated post content + * @param string &$subject New or updated post subject + * @param int $poster_id Post author's user id + * @param int $forum_id The id of the forum in which the post is located * - * @access public + * @access public */ function index($mode, $post_id, &$message, &$subject, $poster_id, $forum_id) { @@ -686,6 +707,8 @@ class phpbb_search_fulltext_sphinx /** * Delete a post from the index after it was deleted + * + * @access public */ function index_remove($post_ids, $author_ids, $forum_ids) { @@ -700,6 +723,8 @@ class phpbb_search_fulltext_sphinx /** * Destroy old cache entries + * + * @access public */ function tidy($create = false) { @@ -724,6 +749,10 @@ class phpbb_search_fulltext_sphinx /** * Create sphinx table + * + * @return string|bool error string is returned incase of errors otherwise false + * + * @access public */ function create_index($acp_module, $u_action) { @@ -758,6 +787,10 @@ class phpbb_search_fulltext_sphinx /** * Drop sphinx table + * + * @return string|bool error string is returned incase of errors otherwise false + * + * @access public */ function delete_index($acp_module, $u_action) { @@ -785,6 +818,10 @@ class phpbb_search_fulltext_sphinx /** * Returns true if the sphinx table was created + * + * @return bool true if sphinx table was created + * + * @access public */ function index_created($allow_new_files = true) { @@ -817,6 +854,8 @@ class phpbb_search_fulltext_sphinx /** * Kills the searchd process and makes sure there's no locks left over + * + * @access private */ function shutdown_searchd() { @@ -846,6 +885,8 @@ class phpbb_search_fulltext_sphinx * files by calling shutdown_searchd. * * @return boolean Whether searchd is running or not + * + * @access private */ function searchd_running() { @@ -890,6 +931,10 @@ class phpbb_search_fulltext_sphinx /** * Returns an associative array containing information about the indexes + * + * @return string|bool Language string of error false otherwise + * + * @access public */ function index_stats() { @@ -910,6 +955,8 @@ class phpbb_search_fulltext_sphinx /** * Collects stats that can be displayed on the index maintenance page + * + * @access private */ function get_stats() { @@ -957,6 +1004,10 @@ class phpbb_search_fulltext_sphinx /** * Returns a list of options for the ACP to display + * + * @return associative array containing template and config variables + * + * @access public */ function acp() { @@ -1112,6 +1163,8 @@ class phpbb_search_fulltext_sphinx * * @param string $path Path from which files shall be deleted * @param string $pattern PCRE pattern that a file needs to match in order to be deleted +* +* @access private */ function sphinx_unlink_by_pattern($path, $pattern) { @@ -1132,6 +1185,8 @@ function sphinx_unlink_by_pattern($path, $pattern) * @param string $file The filename from which the lines shall be read * @param int $amount The number of lines to be read from the end * @return string Last lines of the file +* +* @access private */ function sphinx_read_last_lines($file, $amount) { -- cgit v1.2.1 From e486f4389c99c27cb723d4e9fd437130752f891e Mon Sep 17 00:00:00 2001 From: Dhruv Goel Date: Tue, 10 Jul 2012 03:51:40 +0530 Subject: [feature/sphinx-fulltext-search] remove autoconf Remove all code related to sphinx automatic configuration and all exec calls. PHPBB3-10946 --- phpBB/includes/search/fulltext_sphinx.php | 386 +----------------------------- 1 file changed, 1 insertion(+), 385 deletions(-) (limited to 'phpBB') diff --git a/phpBB/includes/search/fulltext_sphinx.php b/phpBB/includes/search/fulltext_sphinx.php index 82addca28a..984bda68c2 100644 --- a/phpBB/includes/search/fulltext_sphinx.php +++ b/phpBB/includes/search/fulltext_sphinx.php @@ -24,9 +24,6 @@ if (!defined('IN_PHPBB')) global $phpbb_root_path, $phpEx, $table_prefix; require($phpbb_root_path . "includes/sphinxapi-0.9.8." . $phpEx); -define('INDEXER_NAME', 'indexer'); -define('SEARCHD_NAME', 'searchd'); - define('MAX_MATCHES', 20000); define('CONNECT_RETRIES', 3); define('CONNECT_WAIT_TIME', 300); @@ -64,15 +61,6 @@ class phpbb_search_fulltext_sphinx if (!empty($config['fulltext_sphinx_configured'])) { - if ($config['fulltext_sphinx_autorun'] && !file_exists($config['fulltext_sphinx_data_path'] . 'searchd.pid') && $this->index_created(true)) - { - $this->shutdown_searchd(); -// $cwd = getcwd(); -// chdir($config['fulltext_sphinx_bin_path']); - exec($config['fulltext_sphinx_bin_path'] . SEARCHD_NAME . ' --config ' . $config['fulltext_sphinx_config_path'] . 'sphinx.conf >> ' . $config['fulltext_sphinx_data_path'] . 'log/searchd-startup.log 2>&1 &'); -// chdir($cwd); - } - // we only support localhost for now $this->sphinx->SetServer('localhost', (isset($config['fulltext_sphinx_port']) && $config['fulltext_sphinx_port']) ? (int) $config['fulltext_sphinx_port'] : 3312); } @@ -133,111 +121,10 @@ class phpbb_search_fulltext_sphinx { global $db, $user, $config, $phpbb_root_path, $phpEx; - if ($config['fulltext_sphinx_autoconf']) - { - $paths = array('fulltext_sphinx_bin_path', 'fulltext_sphinx_config_path', 'fulltext_sphinx_data_path'); - - // check for completeness and add trailing slash if it's not present - foreach ($paths as $path) - { - if (empty($config[$path])) - { - return $user->lang['FULLTEXT_SPHINX_UNCONFIGURED']; - } - if ($config[$path] && substr($config[$path], -1) != '/') - { - set_config($path, $config[$path] . '/'); - } - } - } - - $executables = array( - $config['fulltext_sphinx_bin_path'] . INDEXER_NAME, - $config['fulltext_sphinx_bin_path'] . SEARCHD_NAME, - ); - - if ($config['fulltext_sphinx_autorun']) - { - foreach ($executables as $executable) - { - if (!file_exists($executable)) - { - return sprintf($user->lang['FULLTEXT_SPHINX_FILE_NOT_FOUND'], $executable); - } - - if (!function_exists('exec')) - { - return $user->lang['FULLTEXT_SPHINX_REQUIRES_EXEC']; - } - - $output = array(); - @exec($executable, $output); - - $output = implode("\n", $output); - if (strpos($output, 'Sphinx ') === false) - { - return sprintf($user->lang['FULLTEXT_SPHINX_FILE_NOT_EXECUTABLE'], $executable); - } - } - } - - $writable_paths = array( - $config['fulltext_sphinx_config_path'] => array('config' => 'fulltext_sphinx_autoconf', 'subdir' => false), - $config['fulltext_sphinx_data_path'] => array('config' => 'fulltext_sphinx_autorun', 'subdir' => 'log'), - $config['fulltext_sphinx_data_path'] . 'log/' => array('config' => 'fulltext_sphinx_autorun', 'subdir' => false), - ); - - foreach ($writable_paths as $path => $info) - { - if ($config[$info['config']]) - { - // make sure directory exists - // if we could drop the @ here and figure out whether the file really - // doesn't exist or whether open_basedir is in effect, would be nice - if (!@file_exists($path)) - { - return sprintf($user->lang['FULLTEXT_SPHINX_DIRECTORY_NOT_FOUND'], $path); - } - - // now check if it is writable by storing a simple file - $filename = $path . 'write_test'; - $fp = @fopen($filename, 'wb'); - if ($fp === false) - { - return sprintf($user->lang['FULLTEXT_SPHINX_FILE_NOT_WRITABLE'], $filename); - } - @fclose($fp); - - @unlink($filename); - - if ($info['subdir'] !== false) - { - if (!is_dir($path . $info['subdir'])) - { - mkdir($path . $info['subdir']); - } - } - } - } - - if ($config['fulltext_sphinx_autoconf']) - { include ($phpbb_root_path . 'config.' . $phpEx); // now that we're sure everything was entered correctly, generate a config for the index // we misuse the avatar_salt for this, as it should be unique ;-) - - if (!file_exists($config['fulltext_sphinx_config_path'] . 'sphinx.conf')) - { - $filename = $config['fulltext_sphinx_config_path'] . 'sphinx.conf'; - $fp = @fopen($filename, 'wb'); - if ($fp === false) - { - return sprintf($user->lang['FULLTEXT_SPHINX_FILE_NOT_WRITABLE'], $filename); - } - @fclose($fp); - } - $config_object = new phpbb_search_sphinx_config($config['fulltext_sphinx_config_path'] . 'sphinx.conf'); $config_data = array( @@ -379,12 +266,8 @@ class phpbb_search_fulltext_sphinx } } - $config_object->write($config['fulltext_sphinx_config_path'] . 'sphinx.conf'); - } - set_config('fulltext_sphinx_configured', '1'); - $this->shutdown_searchd(); $this->tidy(); return false; @@ -689,20 +572,6 @@ class phpbb_search_fulltext_sphinx $this->sphinx->UpdateAttributes($this->indexes, array('topic_last_post_time'), $post_updates); } } - - if ($config['fulltext_sphinx_autorun']) - { - if ($this->index_created()) - { - $rotate = ($this->searchd_running()) ? ' --rotate' : ''; - - $cwd = getcwd(); - chdir($config['fulltext_sphinx_bin_path']); - exec('./' . INDEXER_NAME . $rotate . ' --config ' . $config['fulltext_sphinx_config_path'] . 'sphinx.conf index_phpbb_' . $this->id . '_delta >> ' . $config['fulltext_sphinx_data_path'] . 'log/indexer.log 2>&1 &'); - var_dump('./' . INDEXER_NAME . $rotate . ' --config ' . $config['fulltext_sphinx_config_path'] . 'sphinx.conf index_phpbb_' . $this->id . '_delta >> ' . $config['fulltext_sphinx_data_path'] . 'log/indexer.log 2>&1 &'); - chdir($cwd); - } - } } /** @@ -730,20 +599,6 @@ class phpbb_search_fulltext_sphinx { global $config; - if ($config['fulltext_sphinx_autorun']) - { - if ($this->index_created() || $create) - { - $rotate = ($this->searchd_running()) ? ' --rotate' : ''; - - $cwd = getcwd(); - chdir($config['fulltext_sphinx_bin_path']); - exec('./' . INDEXER_NAME . $rotate . ' --config ' . $config['fulltext_sphinx_config_path'] . 'sphinx.conf index_phpbb_' . $this->id . '_main >> ' . $config['fulltext_sphinx_data_path'] . 'log/indexer.log 2>&1 &'); - exec('./' . INDEXER_NAME . $rotate . ' --config ' . $config['fulltext_sphinx_config_path'] . 'sphinx.conf index_phpbb_' . $this->id . '_delta >> ' . $config['fulltext_sphinx_data_path'] . 'log/indexer.log 2>&1 &'); - chdir($cwd); - } - } - set_config('search_last_gc', time(), true); } @@ -758,8 +613,6 @@ class phpbb_search_fulltext_sphinx { global $db, $user, $config; - $this->shutdown_searchd(); - if (!isset($config['fulltext_sphinx_configured']) || !$config['fulltext_sphinx_configured']) { return $user->lang['FULLTEXT_SPHINX_CONFIGURE_FIRST']; @@ -780,8 +633,6 @@ class phpbb_search_fulltext_sphinx // start indexing process $this->tidy(true); - $this->shutdown_searchd(); - return false; } @@ -796,13 +647,6 @@ class phpbb_search_fulltext_sphinx { global $db, $config; - $this->shutdown_searchd(); - - if ($config['fulltext_sphinx_autorun']) - { - sphinx_unlink_by_pattern($config['fulltext_sphinx_data_path'], '#^index_phpbb_' . $this->id . '.*$#'); - } - if (!$this->index_created()) { return false; @@ -811,8 +655,6 @@ class phpbb_search_fulltext_sphinx $sql = 'DROP TABLE ' . SPHINX_TABLE; $db->sql_query($sql); - $this->shutdown_searchd(); - return false; } @@ -836,99 +678,12 @@ class phpbb_search_fulltext_sphinx if ($row) { - if ($config['fulltext_sphinx_autorun']) - { - if ((file_exists($config['fulltext_sphinx_data_path'] . 'index_phpbb_' . $this->id . '_main.spd') && file_exists($config['fulltext_sphinx_data_path'] . 'index_phpbb_' . $this->id . '_delta.spd')) || ($allow_new_files && file_exists($config['fulltext_sphinx_data_path'] . 'index_phpbb_' . $this->id . '_main.new.spd') && file_exists($config['fulltext_sphinx_data_path'] . 'index_phpbb_' . $this->id . '_delta.new.spd'))) - { - $created = true; - } - } - else - { - $created = true; - } + $created = true; } return $created; } - /** - * Kills the searchd process and makes sure there's no locks left over - * - * @access private - */ - function shutdown_searchd() - { - global $config; - - if ($config['fulltext_sphinx_autorun']) - { - if (!function_exists('exec')) - { - set_config('fulltext_sphinx_autorun', '0'); - return; - } - - exec('killall -9 ' . SEARCHD_NAME . ' >> /dev/null 2>&1 &'); - - if (file_exists($config['fulltext_sphinx_data_path'] . 'searchd.pid')) - { - unlink($config['fulltext_sphinx_data_path'] . 'searchd.pid'); - } - - sphinx_unlink_by_pattern($config['fulltext_sphinx_data_path'], '#^.*\.spl$#'); - } - } - - /** - * Checks whether searchd is running, if it's not running it makes sure there's no left over - * files by calling shutdown_searchd. - * - * @return boolean Whether searchd is running or not - * - * @access private - */ - function searchd_running() - { - global $config; - - // if we cannot manipulate the service assume it is running - if (!$config['fulltext_sphinx_autorun']) - { - return true; - } - - if (file_exists($config['fulltext_sphinx_data_path'] . 'searchd.pid')) - { - $pid = trim(file_get_contents($config['fulltext_sphinx_data_path'] . 'searchd.pid')); - - if ($pid) - { - $output = array(); - $pidof_command = 'pidof'; - - exec('whereis -b pidof', $output); - if (sizeof($output) > 1) - { - $output = explode(' ', trim($output[0])); - $pidof_command = $output[1]; // 0 is pidof: - } - - $output = array(); - exec($pidof_command . ' ' . SEARCHD_NAME, $output); - if (sizeof($output) && (trim($output[0]) == $pid || trim($output[1]) == $pid)) - { - return true; - } - } - } - - // make sure it's really not running - $this->shutdown_searchd(); - - return false; - } - /** * Returns an associative array containing information about the indexes * @@ -980,26 +735,6 @@ class phpbb_search_fulltext_sphinx } $this->stats['last_searches'] = ''; - if ($config['fulltext_sphinx_autorun']) - { - if (file_exists($config['fulltext_sphinx_data_path'] . 'log/sphinx-query.log')) - { - $last_searches = explode("\n", utf8_htmlspecialchars(sphinx_read_last_lines($config['fulltext_sphinx_data_path'] . 'log/sphinx-query.log', 3))); - - foreach($last_searches as $i => $search) - { - if (strpos($search, '[' . $this->indexes . ']') !== false) - { - $last_searches[$i] = str_replace('[' . $this->indexes . ']', '', $search); - } - else - { - $last_searches[$i] = ''; - } - } - $this->stats['last_searches'] = implode("\n", $last_searches); - } - } } /** @@ -1014,8 +749,6 @@ class phpbb_search_fulltext_sphinx global $user, $config; $config_vars = array( - 'fulltext_sphinx_autoconf' => 'bool', - 'fulltext_sphinx_autorun' => 'bool', 'fulltext_sphinx_config_path' => 'string', 'fulltext_sphinx_data_path' => 'string', 'fulltext_sphinx_bin_path' => 'string', @@ -1025,8 +758,6 @@ class phpbb_search_fulltext_sphinx ); $defaults = array( - 'fulltext_sphinx_autoconf' => '1', - 'fulltext_sphinx_autorun' => '1', 'fulltext_sphinx_indexer_mem_limit' => '512', ); @@ -1043,57 +774,8 @@ class phpbb_search_fulltext_sphinx } } - $no_autoconf = false; - $no_autorun = false; $bin_path = $config['fulltext_sphinx_bin_path']; - // try to guess the path if it is empty - if (empty($bin_path)) - { - if (@file_exists('/usr/local/bin/' . INDEXER_NAME) && @file_exists('/usr/local/bin/' . SEARCHD_NAME)) - { - $bin_path = '/usr/local/bin/'; - } - else if (@file_exists('/usr/bin/' . INDEXER_NAME) && @file_exists('/usr/bin/' . SEARCHD_NAME)) - { - $bin_path = '/usr/bin/'; - } - else - { - $output = array(); - if (!function_exists('exec') || null === @exec('whereis -b ' . INDEXER_NAME, $output)) - { - $no_autorun = true; - } - else if (sizeof($output)) - { - $output = explode(' ', $output[0]); - array_shift($output); // remove indexer: - - foreach ($output as $path) - { - $path = dirname($path) . '/'; - - if (file_exists($path . INDEXER_NAME) && file_exists($path . SEARCHD_NAME)) - { - $bin_path = $path; - break; - } - } - } - } - } - - if ($no_autorun) - { - set_config('fulltext_sphinx_autorun', '0'); - } - - if ($no_autoconf) - { - set_config('fulltext_sphinx_autoconf', '0'); - } - // rewrite config if fulltext sphinx is enabled if ($config['fulltext_sphinx_autoconf'] && isset($config['fulltext_sphinx_configured']) && $config['fulltext_sphinx_configured']) { @@ -1115,14 +797,6 @@ class phpbb_search_fulltext_sphinx $tpl = ' ' . $user->lang['FULLTEXT_SPHINX_CONFIGURE_BEFORE']. ' -
-

' . $user->lang['FULLTEXT_SPHINX_AUTOCONF_EXPLAIN'] . '
-
-
-
-

' . $user->lang['FULLTEXT_SPHINX_AUTORUN_EXPLAIN'] . '
-
-

' . $user->lang['FULLTEXT_SPHINX_CONFIG_PATH_EXPLAIN'] . '
@@ -1157,61 +831,3 @@ class phpbb_search_fulltext_sphinx ); } } - -/** -* Deletes all files from a directory that match a certain pattern -* -* @param string $path Path from which files shall be deleted -* @param string $pattern PCRE pattern that a file needs to match in order to be deleted -* -* @access private -*/ -function sphinx_unlink_by_pattern($path, $pattern) -{ - $dir = opendir($path); - while (false !== ($file = readdir($dir))) - { - if (is_file($path . $file) && preg_match($pattern, $file)) - { - unlink($path . $file); - } - } - closedir($dir); -} - -/** -* Reads the last from a file -* -* @param string $file The filename from which the lines shall be read -* @param int $amount The number of lines to be read from the end -* @return string Last lines of the file -* -* @access private -*/ -function sphinx_read_last_lines($file, $amount) -{ - $fp = fopen($file, 'r'); - fseek($fp, 0, SEEK_END); - - $c = ''; - $i = 0; - - while ($i < $amount) - { - fseek($fp, -2, SEEK_CUR); - $c = fgetc($fp); - if ($c == "\n") - { - $i++; - } - if (feof($fp)) - { - break; - } - } - - $string = fread($fp, 8192); - fclose($fp); - - return $string; -} -- cgit v1.2.1 From 88089194e570edb77240138695034358062ffa58 Mon Sep 17 00:00:00 2001 From: Dhruv Goel Date: Tue, 10 Jul 2012 03:55:23 +0530 Subject: [feature/sphinx-fulltext-search] prefix sphinx with constant names All constant names are prefixed with SPHINX_ PHPBB3-10946 --- phpBB/includes/search/fulltext_sphinx.php | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) (limited to 'phpBB') diff --git a/phpBB/includes/search/fulltext_sphinx.php b/phpBB/includes/search/fulltext_sphinx.php index 984bda68c2..7386833151 100644 --- a/phpBB/includes/search/fulltext_sphinx.php +++ b/phpBB/includes/search/fulltext_sphinx.php @@ -24,9 +24,9 @@ if (!defined('IN_PHPBB')) global $phpbb_root_path, $phpEx, $table_prefix; require($phpbb_root_path . "includes/sphinxapi-0.9.8." . $phpEx); -define('MAX_MATCHES', 20000); -define('CONNECT_RETRIES', 3); -define('CONNECT_WAIT_TIME', 300); +define('SPHINX_MAX_MATCHES', 20000); +define('SPHINX_CONNECT_RETRIES', 3); +define('SPHINX_CONNECT_WAIT_TIME', 300); /** * fulltext_sphinx @@ -216,7 +216,7 @@ class phpbb_search_fulltext_sphinx array('read_timeout', '5'), array('max_children', '30'), array('pid_file', $config['fulltext_sphinx_data_path'] . "searchd.pid"), - array('max_matches', (string) MAX_MATCHES), + array('max_matches', (string) SPHINX_MAX_MATCHES), array('binlog_path', $config['fulltext_sphinx_data_path']), ), ); @@ -451,14 +451,14 @@ class phpbb_search_fulltext_sphinx $this->sphinx->SetFilter('deleted', array(0)); - $this->sphinx->SetLimits($start, (int) $per_page, MAX_MATCHES); + $this->sphinx->SetLimits($start, (int) $per_page, SPHINX_MAX_MATCHES); $result = $this->sphinx->Query($search_query_prefix . str_replace('"', '"', $this->search_query), $this->indexes); // could be connection to localhost:3312 failed (errno=111, msg=Connection refused) during rotate, retry if so - $retries = CONNECT_RETRIES; + $retries = SPHINX_CONNECT_RETRIES; while (!$result && (strpos($this->sphinx->_error, "errno=111,") !== false) && $retries--) { - usleep(CONNECT_WAIT_TIME); + usleep(SPHINX_CONNECT_WAIT_TIME); $result = $this->sphinx->Query($search_query_prefix . str_replace('"', '"', $this->search_query), $this->indexes); } -- cgit v1.2.1 From 569db1471b3000512232732a790d8653250e8012 Mon Sep 17 00:00:00 2001 From: Dhruv Goel Date: Tue, 10 Jul 2012 04:15:35 +0530 Subject: [feature/sphinx-fulltext-search] fix stopwords option Stopwords option can be configured in ACP to generate correct sphinx config file. PHPBB3-10946 --- phpBB/includes/search/fulltext_sphinx.php | 28 ++++------------------------ 1 file changed, 4 insertions(+), 24 deletions(-) (limited to 'phpBB') diff --git a/phpBB/includes/search/fulltext_sphinx.php b/phpBB/includes/search/fulltext_sphinx.php index 7386833151..710c3d56be 100644 --- a/phpBB/includes/search/fulltext_sphinx.php +++ b/phpBB/includes/search/fulltext_sphinx.php @@ -193,7 +193,7 @@ class phpbb_search_fulltext_sphinx array('source', 'source_phpbb_' . $this->id . '_main'), array('docinfo', 'extern'), array('morphology', 'none'), - array('stopwords', (file_exists($config['fulltext_sphinx_config_path'] . 'sphinx_stopwords.txt') && $config['fulltext_sphinx_stopwords']) ? $config['fulltext_sphinx_config_path'] . 'sphinx_stopwords.txt' : ''), + array('stopwords', $config['fulltext_sphinx_stopwords'] ? $config['fulltext_sphinx_config_path'] . 'sphinx_stopwords.txt' : ''), array('min_word_len', '2'), array('charset_type', 'utf-8'), array('charset_table', 'U+FF10..U+FF19->0..9, 0..9, U+FF41..U+FF5A->a..z, U+FF21..U+FF3A->a..z, A..Z->a..z, a..z, U+0149, U+017F, U+0138, U+00DF, U+00FF, U+00C0..U+00D6->U+00E0..U+00F6, U+00E0..U+00F6, U+00D8..U+00DE->U+00F8..U+00FE, U+00F8..U+00FE, U+0100->U+0101, U+0101, U+0102->U+0103, U+0103, U+0104->U+0105, U+0105, U+0106->U+0107, U+0107, U+0108->U+0109, U+0109, U+010A->U+010B, U+010B, U+010C->U+010D, U+010D, U+010E->U+010F, U+010F, U+0110->U+0111, U+0111, U+0112->U+0113, U+0113, U+0114->U+0115, U+0115, U+0116->U+0117, U+0117, U+0118->U+0119, U+0119, U+011A->U+011B, U+011B, U+011C->U+011D, U+011D, U+011E->U+011F, U+011F, U+0130->U+0131, U+0131, U+0132->U+0133, U+0133, U+0134->U+0135, U+0135, U+0136->U+0137, U+0137, U+0139->U+013A, U+013A, U+013B->U+013C, U+013C, U+013D->U+013E, U+013E, U+013F->U+0140, U+0140, U+0141->U+0142, U+0142, U+0143->U+0144, U+0144, U+0145->U+0146, U+0146, U+0147->U+0148, U+0148, U+014A->U+014B, U+014B, U+014C->U+014D, U+014D, U+014E->U+014F, U+014F, U+0150->U+0151, U+0151, U+0152->U+0153, U+0153, U+0154->U+0155, U+0155, U+0156->U+0157, U+0157, U+0158->U+0159, U+0159, U+015A->U+015B, U+015B, U+015C->U+015D, U+015D, U+015E->U+015F, U+015F, U+0160->U+0161, U+0161, U+0162->U+0163, U+0163, U+0164->U+0165, U+0165, U+0166->U+0167, U+0167, U+0168->U+0169, U+0169, U+016A->U+016B, U+016B, U+016C->U+016D, U+016D, U+016E->U+016F, U+016F, U+0170->U+0171, U+0171, U+0172->U+0173, U+0173, U+0174->U+0175, U+0175, U+0176->U+0177, U+0177, U+0178->U+00FF, U+00FF, U+0179->U+017A, U+017A, U+017B->U+017C, U+017C, U+017D->U+017E, U+017E, U+0410..U+042F->U+0430..U+044F, U+0430..U+044F, U+4E00..U+9FFF'), @@ -759,6 +759,7 @@ class phpbb_search_fulltext_sphinx $defaults = array( 'fulltext_sphinx_indexer_mem_limit' => '512', + 'fulltext_sphinx_stopwords' => '0', ); foreach ($config_vars as $config_var => $type) @@ -774,27 +775,6 @@ class phpbb_search_fulltext_sphinx } } - $bin_path = $config['fulltext_sphinx_bin_path']; - - // rewrite config if fulltext sphinx is enabled - if ($config['fulltext_sphinx_autoconf'] && isset($config['fulltext_sphinx_configured']) && $config['fulltext_sphinx_configured']) - { - $this->config_updated(); - } - - // check whether stopwords file is available and enabled - if (@file_exists($config['fulltext_sphinx_config_path'] . 'sphinx_stopwords.txt')) - { - $stopwords_available = true; - $stopwords_active = $config['fulltext_sphinx_stopwords']; - } - else - { - $stopwords_available = false; - $stopwords_active = false; - set_config('fulltext_sphinx_stopwords', '0'); - } - $tpl = ' ' . $user->lang['FULLTEXT_SPHINX_CONFIGURE_BEFORE']. '
@@ -809,11 +789,11 @@ class phpbb_search_fulltext_sphinx

' . $user->lang['FULLTEXT_SPHINX_DATA_PATH_EXPLAIN'] . '
- ' . $user->lang['FULLTEXT_SPHINX_CONFIGURE_AFTER']. '

' . $user->lang['FULLTEXT_SPHINX_STOPWORDS_FILE_EXPLAIN'] . '
-
+
+ ' . $user->lang['FULLTEXT_SPHINX_CONFIGURE_AFTER']. '

' . $user->lang['FULLTEXT_SPHINX_PORT_EXPLAIN'] . '
-- cgit v1.2.1 From d2e42d7d619100695e0efe8d472c71f61cbfcb45 Mon Sep 17 00:00:00 2001 From: Dhruv Goel Date: Tue, 10 Jul 2012 04:53:51 +0530 Subject: [feature/sphinx-fulltext-search] remove unnecessary code Some extra conditions and variables used in autoconf are removed. PHPBB3-10946 --- phpBB/includes/search/fulltext_sphinx.php | 25 +++++-------------------- 1 file changed, 5 insertions(+), 20 deletions(-) (limited to 'phpBB') diff --git a/phpBB/includes/search/fulltext_sphinx.php b/phpBB/includes/search/fulltext_sphinx.php index 710c3d56be..48855ef7d8 100644 --- a/phpBB/includes/search/fulltext_sphinx.php +++ b/phpBB/includes/search/fulltext_sphinx.php @@ -59,11 +59,8 @@ class phpbb_search_fulltext_sphinx $this->sphinx = new SphinxClient(); - if (!empty($config['fulltext_sphinx_configured'])) - { - // we only support localhost for now - $this->sphinx->SetServer('localhost', (isset($config['fulltext_sphinx_port']) && $config['fulltext_sphinx_port']) ? (int) $config['fulltext_sphinx_port'] : 3312); - } + // We only support localhost for now + $this->sphinx->SetServer('localhost', (isset($config['fulltext_sphinx_port']) && $config['fulltext_sphinx_port']) ? (int) $config['fulltext_sphinx_port'] : 3312); $config['fulltext_sphinx_min_word_len'] = 2; $config['fulltext_sphinx_max_word_len'] = 400; @@ -125,7 +122,7 @@ class phpbb_search_fulltext_sphinx // now that we're sure everything was entered correctly, generate a config for the index // we misuse the avatar_salt for this, as it should be unique ;-) - $config_object = new phpbb_search_sphinx_config($config['fulltext_sphinx_config_path'] . 'sphinx.conf'); + $config_object = new phpbb_search_sphinx_config($config['fulltext_sphinx_config_path'] . 'sphinx.conf'); $config_data = array( 'source source_phpbb_' . $this->id . '_main' => array( @@ -266,10 +263,6 @@ class phpbb_search_fulltext_sphinx } } - set_config('fulltext_sphinx_configured', '1'); - - $this->tidy(); - return false; } @@ -613,11 +606,6 @@ class phpbb_search_fulltext_sphinx { global $db, $user, $config; - if (!isset($config['fulltext_sphinx_configured']) || !$config['fulltext_sphinx_configured']) - { - return $user->lang['FULLTEXT_SPHINX_CONFIGURE_FIRST']; - } - if (!$this->index_created()) { $sql = 'CREATE TABLE IF NOT EXISTS ' . SPHINX_TABLE . ' ( @@ -630,9 +618,6 @@ class phpbb_search_fulltext_sphinx $db->sql_query($sql); } - // start indexing process - $this->tidy(true); - return false; } @@ -645,7 +630,7 @@ class phpbb_search_fulltext_sphinx */ function delete_index($acp_module, $u_action) { - global $db, $config; + global $db; if (!$this->index_created()) { @@ -715,7 +700,7 @@ class phpbb_search_fulltext_sphinx */ function get_stats() { - global $db, $config; + global $db; if ($this->index_created()) { -- cgit v1.2.1 From 9711da2763f707408efde160357d51330fd17681 Mon Sep 17 00:00:00 2001 From: Dhruv Goel Date: Tue, 10 Jul 2012 05:04:14 +0530 Subject: [feature/sphinx-fulltext-search] adds default config values Default config values are added to config table in new install as well as database_update. PHPBB3-10946 --- phpBB/includes/search/fulltext_sphinx.php | 18 ------------------ phpBB/install/database_update.php | 10 ++++++++++ phpBB/install/schemas/schema_data.sql | 2 ++ 3 files changed, 12 insertions(+), 18 deletions(-) (limited to 'phpBB') diff --git a/phpBB/includes/search/fulltext_sphinx.php b/phpBB/includes/search/fulltext_sphinx.php index 48855ef7d8..36c5c68a3b 100644 --- a/phpBB/includes/search/fulltext_sphinx.php +++ b/phpBB/includes/search/fulltext_sphinx.php @@ -742,24 +742,6 @@ class phpbb_search_fulltext_sphinx 'fulltext_sphinx_indexer_mem_limit' => 'int', ); - $defaults = array( - 'fulltext_sphinx_indexer_mem_limit' => '512', - 'fulltext_sphinx_stopwords' => '0', - ); - - foreach ($config_vars as $config_var => $type) - { - if (!isset($config[$config_var])) - { - $default = ''; - if (isset($defaults[$config_var])) - { - $default = $defaults[$config_var]; - } - set_config($config_var, $default); - } - } - $tpl = ' ' . $user->lang['FULLTEXT_SPHINX_CONFIGURE_BEFORE']. '
diff --git a/phpBB/install/database_update.php b/phpBB/install/database_update.php index 594397c815..0ffab8e413 100644 --- a/phpBB/install/database_update.php +++ b/phpBB/install/database_update.php @@ -2260,6 +2260,16 @@ function change_database_data(&$no_updates, $version) set_config('fulltext_postgres_max_word_len', 254); } + if (!isset($config['fulltext_sphinx_stopwords'])) + { + set_config('fulltext_sphinx_stopwords', 0); + } + + if (!isset($config['fulltext_sphinx_indexer_mem_limit'])) + { + set_config('fulltext_sphinx_indexer_mem_limit', 512); + } + if (!isset($config['load_jquery_cdn'])) { set_config('load_jquery_cdn', 0); diff --git a/phpBB/install/schemas/schema_data.sql b/phpBB/install/schemas/schema_data.sql index deefdafbc4..797f78e889 100644 --- a/phpBB/install/schemas/schema_data.sql +++ b/phpBB/install/schemas/schema_data.sql @@ -128,6 +128,8 @@ INSERT INTO phpbb_config (config_name, config_value) VALUES ('fulltext_native_mi INSERT INTO phpbb_config (config_name, config_value) VALUES ('fulltext_postgres_max_word_len', '254'); INSERT INTO phpbb_config (config_name, config_value) VALUES ('fulltext_postgres_min_word_len', '4'); INSERT INTO phpbb_config (config_name, config_value) VALUES ('fulltext_postgres_ts_name', 'simple'); +INSERT INTO phpbb_config (config_name, config_value) VALUES ('fulltext_sphinx_indexer_mem_limit', '512'); +INSERT INTO phpbb_config (config_name, config_value) VALUES ('fulltext_sphinx_stopwords', '0'); INSERT INTO phpbb_config (config_name, config_value) VALUES ('gzip_compress', '0'); INSERT INTO phpbb_config (config_name, config_value) VALUES ('hot_threshold', '25'); INSERT INTO phpbb_config (config_name, config_value) VALUES ('icons_path', 'images/icons'); -- cgit v1.2.1 From 79432aa4a0698a5a4a518521613aad4adbb40584 Mon Sep 17 00:00:00 2001 From: Dhruv Goel Date: Tue, 10 Jul 2012 05:16:42 +0530 Subject: [feature/sphinx-fulltext-search] assign all globals to class properties PHPBB3-10946 --- phpBB/includes/search/fulltext_sphinx.php | 126 +++++++++++++----------------- 1 file changed, 56 insertions(+), 70 deletions(-) (limited to 'phpBB') diff --git a/phpBB/includes/search/fulltext_sphinx.php b/phpBB/includes/search/fulltext_sphinx.php index 36c5c68a3b..f858b199b2 100644 --- a/phpBB/includes/search/fulltext_sphinx.php +++ b/phpBB/includes/search/fulltext_sphinx.php @@ -40,6 +40,10 @@ class phpbb_search_fulltext_sphinx private $id; private $indexes; private $sphinx; + private $auth; + private $config; + private $db; + private $user; public $word_length = array(); public $search_query; public $common_words = array(); @@ -52,7 +56,11 @@ class phpbb_search_fulltext_sphinx */ public function __construct(&$error) { - global $config; + global $config, $db, $user, $auth; + $this->config = $config; + $this->user = $user; + $this->db = $db; + $this->auth = $auth; $this->id = $config['avatar_salt']; $this->indexes = 'index_phpbb_' . $this->id . '_delta;index_phpbb_' . $this->id . '_main'; @@ -89,11 +97,9 @@ class phpbb_search_fulltext_sphinx */ function init() { - global $db, $user, $config; - - if ($db->sql_layer != 'mysql' && $db->sql_layer != 'mysql4' && $db->sql_layer != 'mysqli') + if ($this->db->sql_layer != 'mysql' && $this->db->sql_layer != 'mysql4' && $this->db->sql_layer != 'mysqli') { - return $user->lang['FULLTEXT_SPHINX_WRONG_DATABASE']; + return $this->user->lang['FULLTEXT_SPHINX_WRONG_DATABASE']; } if ($error = $this->config_updated()) @@ -116,13 +122,13 @@ class phpbb_search_fulltext_sphinx */ function config_updated() { - global $db, $user, $config, $phpbb_root_path, $phpEx; + global $phpbb_root_path, $phpEx; include ($phpbb_root_path . 'config.' . $phpEx); // now that we're sure everything was entered correctly, generate a config for the index // we misuse the avatar_salt for this, as it should be unique ;-) - $config_object = new phpbb_search_sphinx_config($config['fulltext_sphinx_config_path'] . 'sphinx.conf'); + $config_object = new phpbb_search_sphinx_config($this->config['fulltext_sphinx_config_path'] . 'sphinx.conf'); $config_data = array( 'source source_phpbb_' . $this->id . '_main' => array( @@ -186,11 +192,11 @@ class phpbb_search_fulltext_sphinx AND p.post_id >= ( SELECT max_doc_id FROM ' . SPHINX_TABLE . ' WHERE counter_id=1 )'), ), 'index index_phpbb_' . $this->id . '_main' => array( - array('path', $config['fulltext_sphinx_data_path'] . 'index_phpbb_' . $this->id . '_main'), + array('path', $this->config['fulltext_sphinx_data_path'] . 'index_phpbb_' . $this->id . '_main'), array('source', 'source_phpbb_' . $this->id . '_main'), array('docinfo', 'extern'), array('morphology', 'none'), - array('stopwords', $config['fulltext_sphinx_stopwords'] ? $config['fulltext_sphinx_config_path'] . 'sphinx_stopwords.txt' : ''), + array('stopwords', $this->config['fulltext_sphinx_stopwords'] ? $this->config['fulltext_sphinx_config_path'] . 'sphinx_stopwords.txt' : ''), array('min_word_len', '2'), array('charset_type', 'utf-8'), array('charset_table', 'U+FF10..U+FF19->0..9, 0..9, U+FF41..U+FF5A->a..z, U+FF21..U+FF3A->a..z, A..Z->a..z, a..z, U+0149, U+017F, U+0138, U+00DF, U+00FF, U+00C0..U+00D6->U+00E0..U+00F6, U+00E0..U+00F6, U+00D8..U+00DE->U+00F8..U+00FE, U+00F8..U+00FE, U+0100->U+0101, U+0101, U+0102->U+0103, U+0103, U+0104->U+0105, U+0105, U+0106->U+0107, U+0107, U+0108->U+0109, U+0109, U+010A->U+010B, U+010B, U+010C->U+010D, U+010D, U+010E->U+010F, U+010F, U+0110->U+0111, U+0111, U+0112->U+0113, U+0113, U+0114->U+0115, U+0115, U+0116->U+0117, U+0117, U+0118->U+0119, U+0119, U+011A->U+011B, U+011B, U+011C->U+011D, U+011D, U+011E->U+011F, U+011F, U+0130->U+0131, U+0131, U+0132->U+0133, U+0133, U+0134->U+0135, U+0135, U+0136->U+0137, U+0137, U+0139->U+013A, U+013A, U+013B->U+013C, U+013C, U+013D->U+013E, U+013E, U+013F->U+0140, U+0140, U+0141->U+0142, U+0142, U+0143->U+0144, U+0144, U+0145->U+0146, U+0146, U+0147->U+0148, U+0148, U+014A->U+014B, U+014B, U+014C->U+014D, U+014D, U+014E->U+014F, U+014F, U+0150->U+0151, U+0151, U+0152->U+0153, U+0153, U+0154->U+0155, U+0155, U+0156->U+0157, U+0157, U+0158->U+0159, U+0159, U+015A->U+015B, U+015B, U+015C->U+015D, U+015D, U+015E->U+015F, U+015F, U+0160->U+0161, U+0161, U+0162->U+0163, U+0163, U+0164->U+0165, U+0165, U+0166->U+0167, U+0167, U+0168->U+0169, U+0169, U+016A->U+016B, U+016B, U+016C->U+016D, U+016D, U+016E->U+016F, U+016F, U+0170->U+0171, U+0171, U+0172->U+0173, U+0173, U+0174->U+0175, U+0175, U+0176->U+0177, U+0177, U+0178->U+00FF, U+00FF, U+0179->U+017A, U+017A, U+017B->U+017C, U+017C, U+017D->U+017E, U+017E, U+0410..U+042F->U+0430..U+044F, U+0430..U+044F, U+4E00..U+9FFF'), @@ -198,23 +204,23 @@ class phpbb_search_fulltext_sphinx array('min_infix_len', '0'), ), 'index index_phpbb_' . $this->id . '_delta : index_phpbb_' . $this->id . '_main' => array( - array('path', $config['fulltext_sphinx_data_path'] . 'index_phpbb_' . $this->id . '_delta'), + array('path', $this->config['fulltext_sphinx_data_path'] . 'index_phpbb_' . $this->id . '_delta'), array('source', 'source_phpbb_' . $this->id . '_delta'), ), 'indexer' => array( - array('mem_limit', $config['fulltext_sphinx_indexer_mem_limit'] . 'M'), + array('mem_limit', $this->config['fulltext_sphinx_indexer_mem_limit'] . 'M'), ), 'searchd' => array( array('compat_sphinxql_magics' , '0'), array('listen' , '127.0.0.1'), - array('port', ($config['fulltext_sphinx_port']) ? $config['fulltext_sphinx_port'] : '3312'), - array('log', $config['fulltext_sphinx_data_path'] . "log/searchd.log"), - array('query_log', $config['fulltext_sphinx_data_path'] . "log/sphinx-query.log"), + array('port', ($this->config['fulltext_sphinx_port']) ? $this->config['fulltext_sphinx_port'] : '3312'), + array('log', $this->config['fulltext_sphinx_data_path'] . "log/searchd.log"), + array('query_log', $this->config['fulltext_sphinx_data_path'] . "log/sphinx-query.log"), array('read_timeout', '5'), array('max_children', '30'), - array('pid_file', $config['fulltext_sphinx_data_path'] . "searchd.pid"), + array('pid_file', $this->config['fulltext_sphinx_data_path'] . "searchd.pid"), array('max_matches', (string) SPHINX_MAX_MATCHES), - array('binlog_path', $config['fulltext_sphinx_data_path']), + array('binlog_path', $this->config['fulltext_sphinx_data_path']), ), ); @@ -278,8 +284,6 @@ class phpbb_search_fulltext_sphinx */ function split_keywords(&$keywords, $terms) { - global $config; - if ($terms == 'all') { $match = array('#\sand\s#i', '#\sor\s#i', '#\snot\s#i', '#\+#', '#-#', '#\|#', '#@#'); @@ -330,8 +334,6 @@ class phpbb_search_fulltext_sphinx */ function keyword_search($type, $fields, $terms, $sort_by_sql, $sort_key, $sort_dir, $sort_days, $ex_fid_ary, $m_approve_fid_ary, $topic_id, $author_ary, $author_name, &$id_ary, $start, $per_page) { - global $config, $db, $auth; - // No keywords? No posts. if (!strlen($this->search_query) && !sizeof($author_ary)) { @@ -432,7 +434,7 @@ class phpbb_search_fulltext_sphinx if (sizeof($ex_fid_ary)) { // All forums that a user is allowed to access - $fid_ary = array_unique(array_intersect(array_keys($auth->acl_getf('f_read', true)), array_keys($auth->acl_getf('f_search', true)))); + $fid_ary = array_unique(array_intersect(array_keys($this->auth->acl_getf('f_read', true)), array_keys($auth->acl_getf('f_search', true)))); // All forums that the user wants to and can search in $search_forums = array_diff($fid_ary, $ex_fid_ary); @@ -527,8 +529,6 @@ class phpbb_search_fulltext_sphinx */ function index($mode, $post_id, &$message, &$subject, $poster_id, $forum_id) { - global $config, $db; - if ($mode == 'edit') { $this->sphinx->UpdateAttributes($this->indexes, array('forum_id', 'poster_id'), array((int)$post_id => array((int)$forum_id, (int)$poster_id))); @@ -549,16 +549,16 @@ class phpbb_search_fulltext_sphinx )), ); - $sql = $db->sql_build_query('SELECT', $sql_array); - $result = $db->sql_query($sql); + $sql = $this->db->sql_build_query('SELECT', $sql_array); + $result = $this->db->sql_query($sql); $post_updates = array(); $post_time = time(); - while ($row = $db->sql_fetchrow($result)) + while ($row = $this->db->sql_fetchrow($result)) { $post_updates[(int)$row['post_id']] = array($post_time); } - $db->sql_freeresult($result); + $this->db->sql_freeresult($result); if (sizeof($post_updates)) { @@ -590,8 +590,6 @@ class phpbb_search_fulltext_sphinx */ function tidy($create = false) { - global $config; - set_config('search_last_gc', time(), true); } @@ -604,18 +602,16 @@ class phpbb_search_fulltext_sphinx */ function create_index($acp_module, $u_action) { - global $db, $user, $config; - if (!$this->index_created()) { $sql = 'CREATE TABLE IF NOT EXISTS ' . SPHINX_TABLE . ' ( counter_id INT NOT NULL PRIMARY KEY, max_doc_id INT NOT NULL )'; - $db->sql_query($sql); + $this->db->sql_query($sql); $sql = 'TRUNCATE TABLE ' . SPHINX_TABLE; - $db->sql_query($sql); + $this->db->sql_query($sql); } return false; @@ -630,15 +626,13 @@ class phpbb_search_fulltext_sphinx */ function delete_index($acp_module, $u_action) { - global $db; - if (!$this->index_created()) { return false; } $sql = 'DROP TABLE ' . SPHINX_TABLE; - $db->sql_query($sql); + $this->db->sql_query($sql); return false; } @@ -652,12 +646,10 @@ class phpbb_search_fulltext_sphinx */ function index_created($allow_new_files = true) { - global $db, $config; - $sql = 'SHOW TABLES LIKE \'' . SPHINX_TABLE . '\''; - $result = $db->sql_query($sql); - $row = $db->sql_fetchrow($result); - $db->sql_freeresult($result); + $result = $this->db->sql_query($sql); + $row = $this->db->sql_fetchrow($result); + $this->db->sql_freeresult($result); $created = false; @@ -678,18 +670,16 @@ class phpbb_search_fulltext_sphinx */ function index_stats() { - global $user; - if (empty($this->stats)) { $this->get_stats(); } return array( - $user->lang['FULLTEXT_SPHINX_MAIN_POSTS'] => ($this->index_created()) ? $this->stats['main_posts'] : 0, - $user->lang['FULLTEXT_SPHINX_DELTA_POSTS'] => ($this->index_created()) ? $this->stats['total_posts'] - $this->stats['main_posts'] : 0, - $user->lang['FULLTEXT_MYSQL_TOTAL_POSTS'] => ($this->index_created()) ? $this->stats['total_posts'] : 0, - $user->lang['FULLTEXT_SPHINX_LAST_SEARCHES'] => nl2br($this->stats['last_searches']), + $this->user->lang['FULLTEXT_SPHINX_MAIN_POSTS'] => ($this->index_created()) ? $this->stats['main_posts'] : 0, + $this->user->lang['FULLTEXT_SPHINX_DELTA_POSTS'] => ($this->index_created()) ? $this->stats['total_posts'] - $this->stats['main_posts'] : 0, + $this->user->lang['FULLTEXT_MYSQL_TOTAL_POSTS'] => ($this->index_created()) ? $this->stats['total_posts'] : 0, + $this->user->lang['FULLTEXT_SPHINX_LAST_SEARCHES'] => nl2br($this->stats['last_searches']), ); } @@ -700,23 +690,21 @@ class phpbb_search_fulltext_sphinx */ function get_stats() { - global $db; - if ($this->index_created()) { $sql = 'SELECT COUNT(post_id) as total_posts FROM ' . POSTS_TABLE; - $result = $db->sql_query($sql); - $this->stats['total_posts'] = (int) $db->sql_fetchfield('total_posts'); - $db->sql_freeresult($result); + $result = $this->db->sql_query($sql); + $this->stats['total_posts'] = (int) $this->db->sql_fetchfield('total_posts'); + $this->db->sql_freeresult($result); $sql = 'SELECT COUNT(p.post_id) as main_posts FROM ' . POSTS_TABLE . ' p, ' . SPHINX_TABLE . ' m WHERE p.post_id <= m.max_doc_id AND m.counter_id = 1'; - $result = $db->sql_query($sql); - $this->stats['main_posts'] = (int) $db->sql_fetchfield('main_posts'); - $db->sql_freeresult($result); + $result = $this->db->sql_query($sql); + $this->stats['main_posts'] = (int) $this->db->sql_fetchfield('main_posts'); + $this->db->sql_freeresult($result); } $this->stats['last_searches'] = ''; @@ -731,8 +719,6 @@ class phpbb_search_fulltext_sphinx */ function acp() { - global $user, $config; - $config_vars = array( 'fulltext_sphinx_config_path' => 'string', 'fulltext_sphinx_data_path' => 'string', @@ -743,31 +729,31 @@ class phpbb_search_fulltext_sphinx ); $tpl = ' - ' . $user->lang['FULLTEXT_SPHINX_CONFIGURE_BEFORE']. ' + ' . $this->user->lang['FULLTEXT_SPHINX_CONFIGURE_BEFORE']. '
-

' . $user->lang['FULLTEXT_SPHINX_CONFIG_PATH_EXPLAIN'] . '
-
+

' . $this->user->lang['FULLTEXT_SPHINX_CONFIG_PATH_EXPLAIN'] . '
+
-

' . $user->lang['FULLTEXT_SPHINX_BIN_PATH_EXPLAIN'] . '
+

' . $this->user->lang['FULLTEXT_SPHINX_BIN_PATH_EXPLAIN'] . '
-

' . $user->lang['FULLTEXT_SPHINX_DATA_PATH_EXPLAIN'] . '
-
+

' . $this->user->lang['FULLTEXT_SPHINX_DATA_PATH_EXPLAIN'] . '
+
-

' . $user->lang['FULLTEXT_SPHINX_STOPWORDS_FILE_EXPLAIN'] . '
-
+

' . $this->user->lang['FULLTEXT_SPHINX_STOPWORDS_FILE_EXPLAIN'] . '
+
- ' . $user->lang['FULLTEXT_SPHINX_CONFIGURE_AFTER']. ' + ' . $this->user->lang['FULLTEXT_SPHINX_CONFIGURE_AFTER']. '
-

' . $user->lang['FULLTEXT_SPHINX_PORT_EXPLAIN'] . '
-
+

' . $this->user->lang['FULLTEXT_SPHINX_PORT_EXPLAIN'] . '
+
-

' . $user->lang['FULLTEXT_SPHINX_INDEXER_MEM_LIMIT_EXPLAIN'] . '
-
' . $user->lang['MIB'] . '
+

' . $this->user->lang['FULLTEXT_SPHINX_INDEXER_MEM_LIMIT_EXPLAIN'] . '
+
' . $this->user->lang['MIB'] . '
'; -- cgit v1.2.1 From 45c0956bcf72e59138c3b05e26c73b657298f562 Mon Sep 17 00:00:00 2001 From: Dhruv Goel Date: Tue, 10 Jul 2012 05:23:23 +0530 Subject: [feature/sphinx-fulltext-search] implementing db_tools Use db_tools class for creating/dropping sphinx table. PHPBB3-10946 --- phpBB/includes/search/fulltext_sphinx.php | 27 +++++++++++++++++++-------- 1 file changed, 19 insertions(+), 8 deletions(-) (limited to 'phpBB') diff --git a/phpBB/includes/search/fulltext_sphinx.php b/phpBB/includes/search/fulltext_sphinx.php index f858b199b2..317a35937d 100644 --- a/phpBB/includes/search/fulltext_sphinx.php +++ b/phpBB/includes/search/fulltext_sphinx.php @@ -43,6 +43,7 @@ class phpbb_search_fulltext_sphinx private $auth; private $config; private $db; + private $db_tools; private $user; public $word_length = array(); public $search_query; @@ -56,12 +57,20 @@ class phpbb_search_fulltext_sphinx */ public function __construct(&$error) { - global $config, $db, $user, $auth; + global $config, $db, $user, $auth, $phpbb_root_path, $phpEx; $this->config = $config; $this->user = $user; $this->db = $db; $this->auth = $auth; + if (!class_exists('phpbb_db_tools')) + { + require($phpbb_root_path . 'includes/db/db_tools.' . $phpEx); + } + + // Initialize phpbb_db_tools object + $this->db_tools = new phpbb_db_tools($this->db); + $this->id = $config['avatar_salt']; $this->indexes = 'index_phpbb_' . $this->id . '_delta;index_phpbb_' . $this->id . '_main'; @@ -604,11 +613,14 @@ class phpbb_search_fulltext_sphinx { if (!$this->index_created()) { - $sql = 'CREATE TABLE IF NOT EXISTS ' . SPHINX_TABLE . ' ( - counter_id INT NOT NULL PRIMARY KEY, - max_doc_id INT NOT NULL - )'; - $this->db->sql_query($sql); + $table_data = array( + 'COLUMNS' => array( + 'counter_id' => array('UINT', 0), + 'max_doc_id' => array('UINT', 0), + ), + 'PRIMARY_KEY' => 'counter_id', + ); + $this->db_tools->sql_create_table(SPHINX_TABLE, $table_data); $sql = 'TRUNCATE TABLE ' . SPHINX_TABLE; $this->db->sql_query($sql); @@ -631,8 +643,7 @@ class phpbb_search_fulltext_sphinx return false; } - $sql = 'DROP TABLE ' . SPHINX_TABLE; - $this->db->sql_query($sql); + $this->db_tools->sql_table_drop(SPHINX_TABLE); return false; } -- cgit v1.2.1 From 537a16220ea9561332a17004eec79f2854c68df4 Mon Sep 17 00:00:00 2001 From: Dhruv Goel Date: Tue, 10 Jul 2012 05:33:17 +0530 Subject: [feature/sphinx-fulltext-search] remove recent search queries remove recent search queries from the stats as they can't be retreived and remove other language keys being used no more. PHPBB3-10946 --- phpBB/includes/search/fulltext_sphinx.php | 3 --- phpBB/language/en/acp/search.php | 12 +----------- 2 files changed, 1 insertion(+), 14 deletions(-) (limited to 'phpBB') diff --git a/phpBB/includes/search/fulltext_sphinx.php b/phpBB/includes/search/fulltext_sphinx.php index 317a35937d..1ff6ab21b5 100644 --- a/phpBB/includes/search/fulltext_sphinx.php +++ b/phpBB/includes/search/fulltext_sphinx.php @@ -690,7 +690,6 @@ class phpbb_search_fulltext_sphinx $this->user->lang['FULLTEXT_SPHINX_MAIN_POSTS'] => ($this->index_created()) ? $this->stats['main_posts'] : 0, $this->user->lang['FULLTEXT_SPHINX_DELTA_POSTS'] => ($this->index_created()) ? $this->stats['total_posts'] - $this->stats['main_posts'] : 0, $this->user->lang['FULLTEXT_MYSQL_TOTAL_POSTS'] => ($this->index_created()) ? $this->stats['total_posts'] : 0, - $this->user->lang['FULLTEXT_SPHINX_LAST_SEARCHES'] => nl2br($this->stats['last_searches']), ); } @@ -717,8 +716,6 @@ class phpbb_search_fulltext_sphinx $this->stats['main_posts'] = (int) $this->db->sql_fetchfield('main_posts'); $this->db->sql_freeresult($result); } - - $this->stats['last_searches'] = ''; } /** diff --git a/phpBB/language/en/acp/search.php b/phpBB/language/en/acp/search.php index 3fa7f34c64..14701459eb 100644 --- a/phpBB/language/en/acp/search.php +++ b/phpBB/language/en/acp/search.php @@ -57,6 +57,7 @@ $lang = array_merge($lang, array( 'FULLTEXT_MYSQL_MIN_SEARCH_CHARS_EXPLAIN' => 'Words with at least this many characters will be indexed for searching. You or your host can only change this setting by changing the mysql configuration.', 'FULLTEXT_MYSQL_MAX_SEARCH_CHARS_EXPLAIN' => 'Words with no more than this many characters will be indexed for searching. You or your host can only change this setting by changing the mysql configuration.', +<<<<<<< HEAD 'FULLTEXT_POSTGRES_INCOMPATIBLE_DATABASE' => 'The PostgreSQL fulltext backend can only be used with PostgreSQL.', 'FULLTEXT_POSTGRES_TS_NOT_USABLE' => 'The PostgreSQL fulltext backend can only be used with PostgreSQL 8.3 and above.', 'FULLTEXT_POSTGRES_TOTAL_POSTS' => 'Total number of indexed posts', @@ -69,10 +70,6 @@ $lang = array_merge($lang, array( 'FULLTEXT_POSTGRES_MIN_WORD_LEN_EXPLAIN' => 'Words with at least this many characters will be included in the query to the database.', 'FULLTEXT_POSTGRES_MAX_WORD_LEN_EXPLAIN' => 'Words with no more than this many characters will be included in the query to the database.', - 'FULLTEXT_SPHINX_AUTOCONF' => 'Automatically configure Sphinx', - 'FULLTEXT_SPHINX_AUTOCONF_EXPLAIN' => 'This is the easiest way to install Sphinx, just select the settings here and a config file will be written for you. This requires write permissions on the configuration folder.', - 'FULLTEXT_SPHINX_AUTORUN' => 'Automatically run Sphinx', - 'FULLTEXT_SPHINX_AUTORUN_EXPLAIN' => 'This is the easiest way to run Sphinx. Select the paths in this dialogue and the Sphinx daemon will be started and stopped as needed. You can also create an index from the ACP. If your PHP installation forbids the use of exec you can disable this and run Sphinx manually.', 'FULLTEXT_SPHINX_BIN_PATH' => 'Path to executables directory', 'FULLTEXT_SPHINX_BIN_PATH_EXPLAIN' => 'Skip if autorun is disabled. If this path could not be determined automatically you have to enter the path to the directory in which the sphinx executables indexer and searchd reside.', 'FULLTEXT_SPHINX_CONFIG_PATH' => 'Path to configuration directory', @@ -83,18 +80,11 @@ $lang = array_merge($lang, array( 'FULLTEXT_SPHINX_DATA_PATH' => 'Path to data directory', 'FULLTEXT_SPHINX_DATA_PATH_EXPLAIN' => 'Skip if autorun is disabled. You should create this directory outside the web accessable directories. It has to be writable by the user as which your webserver is running (often www-data or nobody). It will be used to store the indexes and log files.', 'FULLTEXT_SPHINX_DELTA_POSTS' => 'Number of posts in frequently updated delta index', - 'FULLTEXT_SPHINX_DIRECTORY_NOT_FOUND' => 'The directory %s does not exist. Please correct your path settings.', - 'FULLTEXT_SPHINX_FILE_NOT_EXECUTABLE' => 'The file %s is not executable for the webserver.', - 'FULLTEXT_SPHINX_FILE_NOT_FOUND' => 'The file %s does not exist. Please correct your path settings.', - 'FULLTEXT_SPHINX_FILE_NOT_WRITABLE' => 'The file %s cannot be written by the webserver.', 'FULLTEXT_SPHINX_INDEXER_MEM_LIMIT' => 'Indexer memory limit', 'FULLTEXT_SPHINX_INDEXER_MEM_LIMIT_EXPLAIN' => 'This number should at all times be lower than the RAM available on your machine. If you experience periodic performance problems this might be due to the indexer consuming too many resources. It might help to lower the amount of memory available to the indexer.', - 'FULLTEXT_SPHINX_LAST_SEARCHES' => 'Recent search queries', 'FULLTEXT_SPHINX_MAIN_POSTS' => 'Number of posts in main index', 'FULLTEXT_SPHINX_PORT' => 'Sphinx search deamon port', 'FULLTEXT_SPHINX_PORT_EXPLAIN' => 'Port on which the sphinx search deamon on localhost listens. Leave empty to use the default 3312', - 'FULLTEXT_SPHINX_REQUIRES_EXEC' => 'The sphinx plugin for phpBB requires PHP’s exec function which is disabled on your system.', - 'FULLTEXT_SPHINX_UNCONFIGURED' => 'Please set all necessary options in the "Fulltext Sphinx" section of the previous page before you try to activate the sphinx plugin.', 'FULLTEXT_SPHINX_WRONG_DATABASE' => 'The sphinx plugin for phpBB currently only supports MySQL', 'FULLTEXT_SPHINX_STOPWORDS_FILE' => 'Stopwords activated', 'FULLTEXT_SPHINX_STOPWORDS_FILE_EXPLAIN' => 'This setting only works with autoconf enabled. You can place a file called sphinx_stopwords.txt containing one word in each line in your config directory. If this file is present these words will be excluded from the indexing process.', -- cgit v1.2.1 From fdb7e64e29d7b6286b505cf3a75fe53cfdfc7503 Mon Sep 17 00:00:00 2001 From: Dhruv Goel Date: Tue, 10 Jul 2012 05:51:52 +0530 Subject: [feature/sphinx-fulltext-search] fix comments PHPBB3-10946 --- phpBB/includes/search/fulltext_sphinx.php | 34 +++++++++------- phpBB/includes/search/sphinx/config.php | 52 ++++++++++++++----------- phpBB/includes/search/sphinx/config_section.php | 6 +-- 3 files changed, 53 insertions(+), 39 deletions(-) (limited to 'phpBB') diff --git a/phpBB/includes/search/fulltext_sphinx.php b/phpBB/includes/search/fulltext_sphinx.php index 1ff6ab21b5..c72db1862e 100644 --- a/phpBB/includes/search/fulltext_sphinx.php +++ b/phpBB/includes/search/fulltext_sphinx.php @@ -116,7 +116,7 @@ class phpbb_search_fulltext_sphinx return $error; } - // move delta to main index each hour + // Move delta to main index each hour set_config('search_gc', 3600); return false; @@ -135,8 +135,9 @@ class phpbb_search_fulltext_sphinx include ($phpbb_root_path . 'config.' . $phpEx); - // now that we're sure everything was entered correctly, generate a config for the index - // we misuse the avatar_salt for this, as it should be unique ;-) + /* Now that we're sure everything was entered correctly, + generate a config for the index. We misuse the avatar_salt + for this, as it should be unique. */ $config_object = new phpbb_search_sphinx_config($this->config['fulltext_sphinx_config_path'] . 'sphinx.conf'); $config_data = array( @@ -396,7 +397,7 @@ class phpbb_search_fulltext_sphinx } } - // most narrow filters first + // Most narrow filters first if ($topic_id) { $this->sphinx->SetFilter('topic_id', array($topic_id)); @@ -407,31 +408,37 @@ class phpbb_search_fulltext_sphinx switch($fields) { case 'titleonly': - // only search the title + // Only search the title if ($terms == 'all') { $search_query_prefix = '@title '; } - $this->sphinx->SetFieldWeights(array("title" => 5, "data" => 1)); // weight for the title - $this->sphinx->SetFilter('topic_first_post', array(1)); // 1 is first_post, 0 is not first post + // Weight for the title + $this->sphinx->SetFieldWeights(array("title" => 5, "data" => 1)); + // 1 is first_post, 0 is not first post + $this->sphinx->SetFilter('topic_first_post', array(1)); break; case 'msgonly': - // only search the body + // Only search the body if ($terms == 'all') { $search_query_prefix = '@data '; } - $this->sphinx->SetFieldWeights(array("title" => 1, "data" => 5)); // weight for the body + // Weight for the body + $this->sphinx->SetFieldWeights(array("title" => 1, "data" => 5)); break; case 'firstpost': - $this->sphinx->SetFieldWeights(array("title" => 5, "data" => 1)); // more relative weight for the title, also search the body - $this->sphinx->SetFilter('topic_first_post', array(1)); // 1 is first_post, 0 is not first post + // More relative weight for the title, also search the body + $this->sphinx->SetFieldWeights(array("title" => 5, "data" => 1)); + // 1 is first_post, 0 is not first post + $this->sphinx->SetFilter('topic_first_post', array(1)); break; default: - $this->sphinx->SetFieldWeights(array("title" => 5, "data" => 1)); // more relative weight for the title, also search the body + // More relative weight for the title, also search the body + $this->sphinx->SetFieldWeights(array("title" => 5, "data" => 1)); break; } @@ -458,7 +465,8 @@ class phpbb_search_fulltext_sphinx $this->sphinx->SetLimits($start, (int) $per_page, SPHINX_MAX_MATCHES); $result = $this->sphinx->Query($search_query_prefix . str_replace('"', '"', $this->search_query), $this->indexes); - // could be connection to localhost:3312 failed (errno=111, msg=Connection refused) during rotate, retry if so + /* Could be connection to localhost:3312 failed (errno=111, + msg=Connection refused) during rotate, retry if so */ $retries = SPHINX_CONNECT_RETRIES; while (!$result && (strpos($this->sphinx->_error, "errno=111,") !== false) && $retries--) { diff --git a/phpBB/includes/search/sphinx/config.php b/phpBB/includes/search/sphinx/config.php index 966cd0f284..3820eff178 100644 --- a/phpBB/includes/search/sphinx/config.php +++ b/phpBB/includes/search/sphinx/config.php @@ -49,7 +49,7 @@ class phpbb_search_sphinx_config { for ($i = 0, $size = sizeof($this->sections); $i < $size; $i++) { - // make sure this is really a section object and not a comment + // Make sure this is really a section object and not a comment if (($this->sections[$i] instanceof phpbb_search_sphinx_config_section) && $this->sections[$i]->get_name() == $name) { return $this->sections[$i]; @@ -76,7 +76,7 @@ class phpbb_search_sphinx_config */ function read($filename) { - // split the file into lines, we'll process it line by line + // Split the file into lines, we'll process it line by line $config_file = file($filename); $this->sections = array(); @@ -87,8 +87,8 @@ class phpbb_search_sphinx_config foreach ($config_file as $i => $line) { - // if the value of a variable continues to the next line because the line break was escaped - // then we don't trim leading space but treat it as a part of the value + /* If the value of a variable continues to the next line because the line + break was escaped then we don't trim leading space but treat it as a part of the value */ if ($in_value) { $line = rtrim($line); @@ -98,11 +98,11 @@ class phpbb_search_sphinx_config $line = trim($line); } - // if we're not inside a section look for one + // If we're not inside a section look for one if (!$section) { - // add empty lines and comments as comment objects to the section list - // that way they're not deleted when reassembling the file from the sections + /* add empty lines and comments as comment objects to the section list + that way they're not deleted when reassembling the file from the sections*/ if (!$line || $line[0] == '#') { $this->sections[] = new phpbb_search_sphinx_config_comment($config_file[$i]); @@ -110,8 +110,8 @@ class phpbb_search_sphinx_config } else { - // otherwise we scan the line reading the section name until we find - // an opening curly bracket or a comment + /* otherwise we scan the line reading the section name until we find + an opening curly bracket or a comment */ $section_name = ''; $section_name_comment = ''; $found_opening_bracket = false; @@ -137,28 +137,29 @@ class phpbb_search_sphinx_config $section_name .= $line[$j]; } - // and then we create the new section object + // And then we create the new section object $section_name = trim($section_name); $section = new phpbb_search_sphinx_config_section($section_name, $section_name_comment); } } - else // if we're looking for variables inside a section + else { + // If we're looking for variables inside a section $skip_first = false; - // if we're not in a value continuing over the line feed + // If we're not in a value continuing over the line feed if (!$in_value) { - // then add empty lines and comments as comment objects to the variable list - // of this section so they're not deleted on reassembly + /* then add empty lines and comments as comment objects to the variable list + of this section so they're not deleted on reassembly */ if (!$line || $line[0] == '#') { $section->add_variable(new phpbb_search_sphinx_config_comment($config_file[$i])); continue; } - // as long as we haven't yet actually found an opening bracket for this section - // we treat everything as comments so it's not deleted either + /* As long as we haven't yet actually found an opening bracket for this section + we treat everything as comments so it's not deleted either */ if (!$found_opening_bracket) { if ($line[0] == '{') @@ -175,7 +176,8 @@ class phpbb_search_sphinx_config } } - // if we did not find a comment in this line or still add to the previous line's value ... + /* If we did not find a comment in this line or still add to the previous + line's value ... */ if ($line || $in_value) { if (!$in_value) @@ -226,21 +228,24 @@ class phpbb_search_sphinx_config { $value .= "\n"; $in_value = true; - continue 2; // go to the next line and keep processing the value in there + // Go to the next line and keep processing the value in there + continue 2; } $value .= $line[$j]; } } - // if a name and an equal sign were found then we have append a new variable object to the section + /* If a name and an equal sign were found then we have append a + new variable object to the section */ if ($name && $found_assignment) { $section->add_variable(new phpbb_search_sphinx_config_variable(trim($name), trim($value), ($end_section) ? '' : $comment)); continue; } - // if we found a closing curly bracket this section has been completed and we can append it to the section list - // and continue with looking for the next section + /* if we found a closing curly bracket this section has been completed + and we can append it to the section list and continue with looking for + the next section */ if ($end_section) { $section->set_end_comment($comment); @@ -250,13 +255,14 @@ class phpbb_search_sphinx_config } } - // if we did not find anything meaningful up to here, then just treat it as a comment + /* If we did not find anything meaningful up to here, then just treat it + as a comment */ $comment = ($skip_first) ? "\t" . substr(ltrim($config_file[$i]), 1) : $config_file[$i]; $section->add_variable(new phpbb_search_sphinx_config_comment($comment)); } } - // keep the filename for later use + // Keep the filename for later use $this->loaded = $filename; } diff --git a/phpBB/includes/search/sphinx/config_section.php b/phpBB/includes/search/sphinx/config_section.php index 529254dd5a..ed20dba279 100644 --- a/phpBB/includes/search/sphinx/config_section.php +++ b/phpBB/includes/search/sphinx/config_section.php @@ -79,7 +79,7 @@ class phpbb_search_sphinx_config_section { for ($i = 0, $size = sizeof($this->variables); $i < $size; $i++) { - // make sure this is a variable object and not a comment + // Make sure this is a variable object and not a comment if (($this->variables[$i] instanceof phpbb_search_sphinx_config_variable) && $this->variables[$i]->get_name() == $name) { return $this->variables[$i]; @@ -96,7 +96,7 @@ class phpbb_search_sphinx_config_section { for ($i = 0, $size = sizeof($this->variables); $i < $size; $i++) { - // make sure this is a variable object and not a comment + // Make sure this is a variable object and not a comment if (($this->variables[$i] instanceof phpbb_search_sphinx_config_variable) && $this->variables[$i]->get_name() == $name) { array_splice($this->variables, $i, 1); @@ -127,7 +127,7 @@ class phpbb_search_sphinx_config_section { $content = $this->name . ' ' . $this->comment . "\n{\n"; - // make sure we don't get too many newlines after the opening bracket + // Make sure we don't get too many newlines after the opening bracket while (trim($this->variables[0]->to_string()) == '') { array_shift($this->variables); -- cgit v1.2.1 From 01261179ce71ff0699ae598828ae82ec98751037 Mon Sep 17 00:00:00 2001 From: Dhruv Goel Date: Tue, 10 Jul 2012 06:07:42 +0530 Subject: [feature/sphinx-fulltext-search] improve formatting PHPBB3-10946 --- phpBB/includes/search/fulltext_sphinx.php | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) (limited to 'phpBB') diff --git a/phpBB/includes/search/fulltext_sphinx.php b/phpBB/includes/search/fulltext_sphinx.php index c72db1862e..5d92c1ff01 100644 --- a/phpBB/includes/search/fulltext_sphinx.php +++ b/phpBB/includes/search/fulltext_sphinx.php @@ -22,7 +22,7 @@ if (!defined('IN_PHPBB')) * function and the variables used are in global space. */ global $phpbb_root_path, $phpEx, $table_prefix; -require($phpbb_root_path . "includes/sphinxapi-0.9.8." . $phpEx); +require($phpbb_root_path . 'includes/sphinxapi-0.9.8.' . $phpEx); define('SPHINX_MAX_MATCHES', 20000); define('SPHINX_CONNECT_RETRIES', 3); @@ -224,11 +224,11 @@ class phpbb_search_fulltext_sphinx array('compat_sphinxql_magics' , '0'), array('listen' , '127.0.0.1'), array('port', ($this->config['fulltext_sphinx_port']) ? $this->config['fulltext_sphinx_port'] : '3312'), - array('log', $this->config['fulltext_sphinx_data_path'] . "log/searchd.log"), - array('query_log', $this->config['fulltext_sphinx_data_path'] . "log/sphinx-query.log"), + array('log', $this->config['fulltext_sphinx_data_path'] . 'log/searchd.log'), + array('query_log', $this->config['fulltext_sphinx_data_path'] . 'log/sphinx-query.log'), array('read_timeout', '5'), array('max_children', '30'), - array('pid_file', $this->config['fulltext_sphinx_data_path'] . "searchd.pid"), + array('pid_file', $this->config['fulltext_sphinx_data_path'] . 'searchd.pid'), array('max_matches', (string) SPHINX_MAX_MATCHES), array('binlog_path', $this->config['fulltext_sphinx_data_path']), ), @@ -352,9 +352,9 @@ class phpbb_search_fulltext_sphinx $id_ary = array(); - $join_topic = ($type == 'posts') ? false : true; + $join_topic = ($type != 'posts'); - // sorting + // Sorting if ($type == 'topics') { @@ -405,7 +405,7 @@ class phpbb_search_fulltext_sphinx $search_query_prefix = ''; - switch($fields) + switch ($fields) { case 'titleonly': // Only search the title @@ -483,7 +483,7 @@ class phpbb_search_fulltext_sphinx } else { - foreach($result['matches'] as $key => $value) + foreach ($result['matches'] as $key => $value) { $id_ary[] = $value['attrs']['topic_id']; } -- cgit v1.2.1 From f0692bb9e83f6af9725023028f07cba636d04a4b Mon Sep 17 00:00:00 2001 From: Dhruv Goel Date: Tue, 10 Jul 2012 06:38:36 +0530 Subject: [feature/sphinx-fulltext-search] modify config class Sphinx config class is modified to return the configuration data instead of writing it to a file. Search backend property config_file_data stores the generated data. PHPBB3-10946 --- phpBB/includes/search/fulltext_sphinx.php | 263 +++++++++++++++--------------- phpBB/includes/search/sphinx/config.php | 40 ++--- 2 files changed, 145 insertions(+), 158 deletions(-) (limited to 'phpBB') diff --git a/phpBB/includes/search/fulltext_sphinx.php b/phpBB/includes/search/fulltext_sphinx.php index 5d92c1ff01..a54ebe1a59 100644 --- a/phpBB/includes/search/fulltext_sphinx.php +++ b/phpBB/includes/search/fulltext_sphinx.php @@ -45,6 +45,7 @@ class phpbb_search_fulltext_sphinx private $db; private $db_tools; private $user; + private $config_file_data = ''; public $word_length = array(); public $search_query; public $common_words = array(); @@ -133,151 +134,151 @@ class phpbb_search_fulltext_sphinx { global $phpbb_root_path, $phpEx; - include ($phpbb_root_path . 'config.' . $phpEx); - - /* Now that we're sure everything was entered correctly, - generate a config for the index. We misuse the avatar_salt - for this, as it should be unique. */ - $config_object = new phpbb_search_sphinx_config($this->config['fulltext_sphinx_config_path'] . 'sphinx.conf'); - - $config_data = array( - 'source source_phpbb_' . $this->id . '_main' => array( - array('type', 'mysql'), - array('sql_host', $dbhost), - array('sql_user', $dbuser), - array('sql_pass', $dbpasswd), - array('sql_db', $dbname), - array('sql_port', $dbport), - array('sql_query_pre', 'SET NAMES utf8'), - array('sql_query_pre', 'REPLACE INTO ' . SPHINX_TABLE . ' SELECT 1, MAX(post_id) FROM ' . POSTS_TABLE . ''), - array('sql_query_range', 'SELECT MIN(post_id), MAX(post_id) FROM ' . POSTS_TABLE . ''), - array('sql_range_step', '5000'), - array('sql_query', 'SELECT - p.post_id AS id, - p.forum_id, - p.topic_id, - p.poster_id, - IF(p.post_id = t.topic_first_post_id, 1, 0) as topic_first_post, - p.post_time, - p.post_subject, - p.post_subject as title, - p.post_text as data, - t.topic_last_post_time, - 0 as deleted - FROM ' . POSTS_TABLE . ' p, ' . TOPICS_TABLE . ' t - WHERE - p.topic_id = t.topic_id - AND p.post_id >= $start AND p.post_id <= $end'), - array('sql_query_post', ''), - array('sql_query_post_index', 'REPLACE INTO ' . SPHINX_TABLE . ' ( counter_id, max_doc_id ) VALUES ( 1, $maxid )'), - array('sql_query_info', 'SELECT * FROM ' . POSTS_TABLE . ' WHERE post_id = $id'), - array('sql_attr_uint', 'forum_id'), - array('sql_attr_uint', 'topic_id'), - array('sql_attr_uint', 'poster_id'), - array('sql_attr_bool', 'topic_first_post'), - array('sql_attr_bool', 'deleted'), - array('sql_attr_timestamp' , 'post_time'), - array('sql_attr_timestamp' , 'topic_last_post_time'), - array('sql_attr_str2ordinal', 'post_subject'), - ), - 'source source_phpbb_' . $this->id . '_delta : source_phpbb_' . $this->id . '_main' => array( - array('sql_query_pre', ''), - array('sql_query_range', ''), - array('sql_range_step', ''), - array('sql_query', 'SELECT - p.post_id AS id, - p.forum_id, - p.topic_id, - p.poster_id, - IF(p.post_id = t.topic_first_post_id, 1, 0) as topic_first_post, - p.post_time, - p.post_subject, - p.post_subject as title, - p.post_text as data, - t.topic_last_post_time, - 0 as deleted - FROM ' . POSTS_TABLE . ' p, ' . TOPICS_TABLE . ' t - WHERE - p.topic_id = t.topic_id - AND p.post_id >= ( SELECT max_doc_id FROM ' . SPHINX_TABLE . ' WHERE counter_id=1 )'), - ), - 'index index_phpbb_' . $this->id . '_main' => array( - array('path', $this->config['fulltext_sphinx_data_path'] . 'index_phpbb_' . $this->id . '_main'), - array('source', 'source_phpbb_' . $this->id . '_main'), - array('docinfo', 'extern'), - array('morphology', 'none'), - array('stopwords', $this->config['fulltext_sphinx_stopwords'] ? $this->config['fulltext_sphinx_config_path'] . 'sphinx_stopwords.txt' : ''), - array('min_word_len', '2'), - array('charset_type', 'utf-8'), - array('charset_table', 'U+FF10..U+FF19->0..9, 0..9, U+FF41..U+FF5A->a..z, U+FF21..U+FF3A->a..z, A..Z->a..z, a..z, U+0149, U+017F, U+0138, U+00DF, U+00FF, U+00C0..U+00D6->U+00E0..U+00F6, U+00E0..U+00F6, U+00D8..U+00DE->U+00F8..U+00FE, U+00F8..U+00FE, U+0100->U+0101, U+0101, U+0102->U+0103, U+0103, U+0104->U+0105, U+0105, U+0106->U+0107, U+0107, U+0108->U+0109, U+0109, U+010A->U+010B, U+010B, U+010C->U+010D, U+010D, U+010E->U+010F, U+010F, U+0110->U+0111, U+0111, U+0112->U+0113, U+0113, U+0114->U+0115, U+0115, U+0116->U+0117, U+0117, U+0118->U+0119, U+0119, U+011A->U+011B, U+011B, U+011C->U+011D, U+011D, U+011E->U+011F, U+011F, U+0130->U+0131, U+0131, U+0132->U+0133, U+0133, U+0134->U+0135, U+0135, U+0136->U+0137, U+0137, U+0139->U+013A, U+013A, U+013B->U+013C, U+013C, U+013D->U+013E, U+013E, U+013F->U+0140, U+0140, U+0141->U+0142, U+0142, U+0143->U+0144, U+0144, U+0145->U+0146, U+0146, U+0147->U+0148, U+0148, U+014A->U+014B, U+014B, U+014C->U+014D, U+014D, U+014E->U+014F, U+014F, U+0150->U+0151, U+0151, U+0152->U+0153, U+0153, U+0154->U+0155, U+0155, U+0156->U+0157, U+0157, U+0158->U+0159, U+0159, U+015A->U+015B, U+015B, U+015C->U+015D, U+015D, U+015E->U+015F, U+015F, U+0160->U+0161, U+0161, U+0162->U+0163, U+0163, U+0164->U+0165, U+0165, U+0166->U+0167, U+0167, U+0168->U+0169, U+0169, U+016A->U+016B, U+016B, U+016C->U+016D, U+016D, U+016E->U+016F, U+016F, U+0170->U+0171, U+0171, U+0172->U+0173, U+0173, U+0174->U+0175, U+0175, U+0176->U+0177, U+0177, U+0178->U+00FF, U+00FF, U+0179->U+017A, U+017A, U+017B->U+017C, U+017C, U+017D->U+017E, U+017E, U+0410..U+042F->U+0430..U+044F, U+0430..U+044F, U+4E00..U+9FFF'), - array('min_prefix_len', '0'), - array('min_infix_len', '0'), - ), - 'index index_phpbb_' . $this->id . '_delta : index_phpbb_' . $this->id . '_main' => array( - array('path', $this->config['fulltext_sphinx_data_path'] . 'index_phpbb_' . $this->id . '_delta'), - array('source', 'source_phpbb_' . $this->id . '_delta'), - ), - 'indexer' => array( - array('mem_limit', $this->config['fulltext_sphinx_indexer_mem_limit'] . 'M'), - ), - 'searchd' => array( - array('compat_sphinxql_magics' , '0'), - array('listen' , '127.0.0.1'), - array('port', ($this->config['fulltext_sphinx_port']) ? $this->config['fulltext_sphinx_port'] : '3312'), - array('log', $this->config['fulltext_sphinx_data_path'] . 'log/searchd.log'), - array('query_log', $this->config['fulltext_sphinx_data_path'] . 'log/sphinx-query.log'), - array('read_timeout', '5'), - array('max_children', '30'), - array('pid_file', $this->config['fulltext_sphinx_data_path'] . 'searchd.pid'), - array('max_matches', (string) SPHINX_MAX_MATCHES), - array('binlog_path', $this->config['fulltext_sphinx_data_path']), - ), - ); + include ($phpbb_root_path . 'config.' . $phpEx); + + /* Now that we're sure everything was entered correctly, + generate a config for the index. We misuse the avatar_salt + for this, as it should be unique. */ + $config_object = new phpbb_search_sphinx_config($this->config_file_data); + + $config_data = array( + 'source source_phpbb_' . $this->id . '_main' => array( + array('type', 'mysql'), + array('sql_host', $dbhost), + array('sql_user', $dbuser), + array('sql_pass', $dbpasswd), + array('sql_db', $dbname), + array('sql_port', $dbport), + array('sql_query_pre', 'SET NAMES utf8'), + array('sql_query_pre', 'REPLACE INTO ' . SPHINX_TABLE . ' SELECT 1, MAX(post_id) FROM ' . POSTS_TABLE . ''), + array('sql_query_range', 'SELECT MIN(post_id), MAX(post_id) FROM ' . POSTS_TABLE . ''), + array('sql_range_step', '5000'), + array('sql_query', 'SELECT + p.post_id AS id, + p.forum_id, + p.topic_id, + p.poster_id, + IF(p.post_id = t.topic_first_post_id, 1, 0) as topic_first_post, + p.post_time, + p.post_subject, + p.post_subject as title, + p.post_text as data, + t.topic_last_post_time, + 0 as deleted + FROM ' . POSTS_TABLE . ' p, ' . TOPICS_TABLE . ' t + WHERE + p.topic_id = t.topic_id + AND p.post_id >= $start AND p.post_id <= $end'), + array('sql_query_post', ''), + array('sql_query_post_index', 'REPLACE INTO ' . SPHINX_TABLE . ' ( counter_id, max_doc_id ) VALUES ( 1, $maxid )'), + array('sql_query_info', 'SELECT * FROM ' . POSTS_TABLE . ' WHERE post_id = $id'), + array('sql_attr_uint', 'forum_id'), + array('sql_attr_uint', 'topic_id'), + array('sql_attr_uint', 'poster_id'), + array('sql_attr_bool', 'topic_first_post'), + array('sql_attr_bool', 'deleted'), + array('sql_attr_timestamp' , 'post_time'), + array('sql_attr_timestamp' , 'topic_last_post_time'), + array('sql_attr_str2ordinal', 'post_subject'), + ), + 'source source_phpbb_' . $this->id . '_delta : source_phpbb_' . $this->id . '_main' => array( + array('sql_query_pre', ''), + array('sql_query_range', ''), + array('sql_range_step', ''), + array('sql_query', 'SELECT + p.post_id AS id, + p.forum_id, + p.topic_id, + p.poster_id, + IF(p.post_id = t.topic_first_post_id, 1, 0) as topic_first_post, + p.post_time, + p.post_subject, + p.post_subject as title, + p.post_text as data, + t.topic_last_post_time, + 0 as deleted + FROM ' . POSTS_TABLE . ' p, ' . TOPICS_TABLE . ' t + WHERE + p.topic_id = t.topic_id + AND p.post_id >= ( SELECT max_doc_id FROM ' . SPHINX_TABLE . ' WHERE counter_id=1 )'), + ), + 'index index_phpbb_' . $this->id . '_main' => array( + array('path', $this->config['fulltext_sphinx_data_path'] . 'index_phpbb_' . $this->id . '_main'), + array('source', 'source_phpbb_' . $this->id . '_main'), + array('docinfo', 'extern'), + array('morphology', 'none'), + array('stopwords', $this->config['fulltext_sphinx_stopwords'] ? $this->config['fulltext_sphinx_config_path'] . 'sphinx_stopwords.txt' : ''), + array('min_word_len', '2'), + array('charset_type', 'utf-8'), + array('charset_table', 'U+FF10..U+FF19->0..9, 0..9, U+FF41..U+FF5A->a..z, U+FF21..U+FF3A->a..z, A..Z->a..z, a..z, U+0149, U+017F, U+0138, U+00DF, U+00FF, U+00C0..U+00D6->U+00E0..U+00F6, U+00E0..U+00F6, U+00D8..U+00DE->U+00F8..U+00FE, U+00F8..U+00FE, U+0100->U+0101, U+0101, U+0102->U+0103, U+0103, U+0104->U+0105, U+0105, U+0106->U+0107, U+0107, U+0108->U+0109, U+0109, U+010A->U+010B, U+010B, U+010C->U+010D, U+010D, U+010E->U+010F, U+010F, U+0110->U+0111, U+0111, U+0112->U+0113, U+0113, U+0114->U+0115, U+0115, U+0116->U+0117, U+0117, U+0118->U+0119, U+0119, U+011A->U+011B, U+011B, U+011C->U+011D, U+011D, U+011E->U+011F, U+011F, U+0130->U+0131, U+0131, U+0132->U+0133, U+0133, U+0134->U+0135, U+0135, U+0136->U+0137, U+0137, U+0139->U+013A, U+013A, U+013B->U+013C, U+013C, U+013D->U+013E, U+013E, U+013F->U+0140, U+0140, U+0141->U+0142, U+0142, U+0143->U+0144, U+0144, U+0145->U+0146, U+0146, U+0147->U+0148, U+0148, U+014A->U+014B, U+014B, U+014C->U+014D, U+014D, U+014E->U+014F, U+014F, U+0150->U+0151, U+0151, U+0152->U+0153, U+0153, U+0154->U+0155, U+0155, U+0156->U+0157, U+0157, U+0158->U+0159, U+0159, U+015A->U+015B, U+015B, U+015C->U+015D, U+015D, U+015E->U+015F, U+015F, U+0160->U+0161, U+0161, U+0162->U+0163, U+0163, U+0164->U+0165, U+0165, U+0166->U+0167, U+0167, U+0168->U+0169, U+0169, U+016A->U+016B, U+016B, U+016C->U+016D, U+016D, U+016E->U+016F, U+016F, U+0170->U+0171, U+0171, U+0172->U+0173, U+0173, U+0174->U+0175, U+0175, U+0176->U+0177, U+0177, U+0178->U+00FF, U+00FF, U+0179->U+017A, U+017A, U+017B->U+017C, U+017C, U+017D->U+017E, U+017E, U+0410..U+042F->U+0430..U+044F, U+0430..U+044F, U+4E00..U+9FFF'), + array('min_prefix_len', '0'), + array('min_infix_len', '0'), + ), + 'index index_phpbb_' . $this->id . '_delta : index_phpbb_' . $this->id . '_main' => array( + array('path', $this->config['fulltext_sphinx_data_path'] . 'index_phpbb_' . $this->id . '_delta'), + array('source', 'source_phpbb_' . $this->id . '_delta'), + ), + 'indexer' => array( + array('mem_limit', $this->config['fulltext_sphinx_indexer_mem_limit'] . 'M'), + ), + 'searchd' => array( + array('compat_sphinxql_magics' , '0'), + array('listen' , '127.0.0.1'), + array('port', ($this->config['fulltext_sphinx_port']) ? $this->config['fulltext_sphinx_port'] : '3312'), + array('log', $this->config['fulltext_sphinx_data_path'] . 'log/searchd.log'), + array('query_log', $this->config['fulltext_sphinx_data_path'] . 'log/sphinx-query.log'), + array('read_timeout', '5'), + array('max_children', '30'), + array('pid_file', $this->config['fulltext_sphinx_data_path'] . 'searchd.pid'), + array('max_matches', (string) SPHINX_MAX_MATCHES), + array('binlog_path', $this->config['fulltext_sphinx_data_path']), + ), + ); - $non_unique = array('sql_query_pre' => true, 'sql_attr_uint' => true, 'sql_attr_timestamp' => true, 'sql_attr_str2ordinal' => true, 'sql_attr_bool' => true); - $delete = array('sql_group_column' => true, 'sql_date_column' => true, 'sql_str2ordinal_column' => true); + $non_unique = array('sql_query_pre' => true, 'sql_attr_uint' => true, 'sql_attr_timestamp' => true, 'sql_attr_str2ordinal' => true, 'sql_attr_bool' => true); + $delete = array('sql_group_column' => true, 'sql_date_column' => true, 'sql_str2ordinal_column' => true); + foreach ($config_data as $section_name => $section_data) + { + $section = $config_object->get_section_by_name($section_name); + if (!$section) + { + $section = $config_object->add_section($section_name); + } - foreach ($config_data as $section_name => $section_data) + foreach ($delete as $key => $void) { - $section = $config_object->get_section_by_name($section_name); - if (!$section) - { - $section = $config_object->add_section($section_name); - } + $section->delete_variables_by_name($key); + } - foreach ($delete as $key => $void) - { - $section->delete_variables_by_name($key); - } + foreach ($non_unique as $key => $void) + { + $section->delete_variables_by_name($key); + } - foreach ($non_unique as $key => $void) - { - $section->delete_variables_by_name($key); - } + foreach ($section_data as $entry) + { + $key = $entry[0]; + $value = $entry[1]; - foreach ($section_data as $entry) + if (!isset($non_unique[$key])) { - $key = $entry[0]; - $value = $entry[1]; - - if (!isset($non_unique[$key])) + $variable = $section->get_variable_by_name($key); + if (!$variable) { - $variable = $section->get_variable_by_name($key); - if (!$variable) - { - $variable = $section->create_variable($key, $value); - } - else - { - $variable->set_value($value); - } + $variable = $section->create_variable($key, $value); } else { - $variable = $section->create_variable($key, $value); + $variable->set_value($value); } } + else + { + $variable = $section->create_variable($key, $value); + } } + } + $this->config_file_data = $config_object->get_data(); return false; } diff --git a/phpBB/includes/search/sphinx/config.php b/phpBB/includes/search/sphinx/config.php index 3820eff178..e0ad667fb6 100644 --- a/phpBB/includes/search/sphinx/config.php +++ b/phpBB/includes/search/sphinx/config.php @@ -27,15 +27,15 @@ class phpbb_search_sphinx_config var $sections = array(); /** - * Constructor which optionally loads data from a file + * Constructor which optionally loads data from a variable * - * @param string $filename The path to a file containing the sphinx configuration + * @param string $config_data Variable containing the sphinx configuration data */ - function __construct($filename = false) + function __construct($config_data) { - if ($filename !== false && file_exists($filename)) + if ($config_data != '') { - $this->read($filename); + $this->read($config_data); } } @@ -70,22 +70,19 @@ class phpbb_search_sphinx_config } /** - * Parses the config file at the given path, which is stored in $this->loaded for later use + * Reads the config file data * - * @param string $filename The path to the config file + * @param string $config_data The config file data */ - function read($filename) + function read($config_data) { - // Split the file into lines, we'll process it line by line - $config_file = file($filename); - $this->sections = array(); $section = null; $found_opening_bracket = false; $in_value = false; - foreach ($config_file as $i => $line) + foreach ($config_data as $i => $line) { /* If the value of a variable continues to the next line because the line break was escaped then we don't trim leading space but treat it as a part of the value */ @@ -262,32 +259,21 @@ class phpbb_search_sphinx_config } } - // Keep the filename for later use - $this->loaded = $filename; } /** - * Writes the config data into a file + * Returns the config data * - * @param string $filename The optional filename into which the config data shall be written. - * If it's not specified it will be written into the file that the config - * was originally read from. + * @return string $data The config data that is generated. */ - function write($filename = false) + function get_data() { - if ($filename === false && $this->loaded) - { - $filename = $this->loaded; - } - $data = ""; foreach ($this->sections as $section) { $data .= $section->to_string(); } - $fp = fopen($filename, 'wb'); - fwrite($fp, $data); - fclose($fp); + return $data; } } -- cgit v1.2.1 From b16e70ae1d03587c7d7d7e106299a4e576491751 Mon Sep 17 00:00:00 2001 From: Dhruv Goel Date: Tue, 10 Jul 2012 12:32:42 +0530 Subject: [feature/sphinx-fulltext-search] remove bin_path fulltext_sphinx_bin_path from ACP as it is no longer required. PHPBB3-10946 --- phpBB/includes/search/fulltext_sphinx.php | 5 ----- phpBB/language/en/acp/search.php | 3 --- 2 files changed, 8 deletions(-) (limited to 'phpBB') diff --git a/phpBB/includes/search/fulltext_sphinx.php b/phpBB/includes/search/fulltext_sphinx.php index a54ebe1a59..6488cbcd40 100644 --- a/phpBB/includes/search/fulltext_sphinx.php +++ b/phpBB/includes/search/fulltext_sphinx.php @@ -739,7 +739,6 @@ class phpbb_search_fulltext_sphinx $config_vars = array( 'fulltext_sphinx_config_path' => 'string', 'fulltext_sphinx_data_path' => 'string', - 'fulltext_sphinx_bin_path' => 'string', 'fulltext_sphinx_port' => 'int', 'fulltext_sphinx_stopwords' => 'bool', 'fulltext_sphinx_indexer_mem_limit' => 'int', @@ -751,10 +750,6 @@ class phpbb_search_fulltext_sphinx

' . $this->user->lang['FULLTEXT_SPHINX_CONFIG_PATH_EXPLAIN'] . '
-
-

' . $this->user->lang['FULLTEXT_SPHINX_BIN_PATH_EXPLAIN'] . '
-
-

' . $this->user->lang['FULLTEXT_SPHINX_DATA_PATH_EXPLAIN'] . '
diff --git a/phpBB/language/en/acp/search.php b/phpBB/language/en/acp/search.php index 14701459eb..9618bb90c5 100644 --- a/phpBB/language/en/acp/search.php +++ b/phpBB/language/en/acp/search.php @@ -57,7 +57,6 @@ $lang = array_merge($lang, array( 'FULLTEXT_MYSQL_MIN_SEARCH_CHARS_EXPLAIN' => 'Words with at least this many characters will be indexed for searching. You or your host can only change this setting by changing the mysql configuration.', 'FULLTEXT_MYSQL_MAX_SEARCH_CHARS_EXPLAIN' => 'Words with no more than this many characters will be indexed for searching. You or your host can only change this setting by changing the mysql configuration.', -<<<<<<< HEAD 'FULLTEXT_POSTGRES_INCOMPATIBLE_DATABASE' => 'The PostgreSQL fulltext backend can only be used with PostgreSQL.', 'FULLTEXT_POSTGRES_TS_NOT_USABLE' => 'The PostgreSQL fulltext backend can only be used with PostgreSQL 8.3 and above.', 'FULLTEXT_POSTGRES_TOTAL_POSTS' => 'Total number of indexed posts', @@ -70,8 +69,6 @@ $lang = array_merge($lang, array( 'FULLTEXT_POSTGRES_MIN_WORD_LEN_EXPLAIN' => 'Words with at least this many characters will be included in the query to the database.', 'FULLTEXT_POSTGRES_MAX_WORD_LEN_EXPLAIN' => 'Words with no more than this many characters will be included in the query to the database.', - 'FULLTEXT_SPHINX_BIN_PATH' => 'Path to executables directory', - 'FULLTEXT_SPHINX_BIN_PATH_EXPLAIN' => 'Skip if autorun is disabled. If this path could not be determined automatically you have to enter the path to the directory in which the sphinx executables indexer and searchd reside.', 'FULLTEXT_SPHINX_CONFIG_PATH' => 'Path to configuration directory', 'FULLTEXT_SPHINX_CONFIG_PATH_EXPLAIN' => 'Skip if autoconf is disabled. You should create this config directory outside the web accessable directories. It has to be writable by the user as which your webserver is running (often www-data or nobody).', 'FULLTEXT_SPHINX_CONFIGURE_FIRST' => 'Before you create an index you have to enable and configure sphinx under GENERAL -> SERVER CONFIGURATION -> Search settings.', -- cgit v1.2.1 From 4b40f0d3c6d14adc2b20b866cbeb42586cf8d874 Mon Sep 17 00:00:00 2001 From: Dhruv Date: Wed, 11 Jul 2012 16:25:19 +0530 Subject: [feature/sphinx-fulltext-search] display config file in ACP sphinx config file is generated and displayed in the ACP for user to use it to start sphinx search daemon. PHPBB3-10946 --- phpBB/includes/search/fulltext_sphinx.php | 22 +++++++++++++--------- phpBB/language/en/acp/search.php | 3 +++ 2 files changed, 16 insertions(+), 9 deletions(-) (limited to 'phpBB') diff --git a/phpBB/includes/search/fulltext_sphinx.php b/phpBB/includes/search/fulltext_sphinx.php index 6488cbcd40..6e554eec00 100644 --- a/phpBB/includes/search/fulltext_sphinx.php +++ b/phpBB/includes/search/fulltext_sphinx.php @@ -112,11 +112,6 @@ class phpbb_search_fulltext_sphinx return $this->user->lang['FULLTEXT_SPHINX_WRONG_DATABASE']; } - if ($error = $this->config_updated()) - { - return $error; - } - // Move delta to main index each hour set_config('search_gc', 3600); @@ -124,16 +119,21 @@ class phpbb_search_fulltext_sphinx } /** - * Updates the config file sphinx.conf and generates the same in case autoconf is selected + * Generates content of sphinx.conf * - * @return string|bool Language key of the error/incompatiblity occured otherwise false + * @return bool True if sphinx.conf content is correctly generated, false otherwise * * @access private */ - function config_updated() + function config_generate() { global $phpbb_root_path, $phpEx; + if (!$this->config['fulltext_sphinx_data_path'] || !$this->config['fulltext_sphinx_config_path']) + { + return false; + } + include ($phpbb_root_path . 'config.' . $phpEx); /* Now that we're sure everything was entered correctly, @@ -280,7 +280,7 @@ class phpbb_search_fulltext_sphinx } $this->config_file_data = $config_object->get_data(); - return false; + return true; } /** @@ -767,6 +767,10 @@ class phpbb_search_fulltext_sphinx

' . $this->user->lang['FULLTEXT_SPHINX_INDEXER_MEM_LIMIT_EXPLAIN'] . '
' . $this->user->lang['MIB'] . '
+
+

' . $this->user->lang['FULLTEXT_SPHINX_CONFIG_FILE_EXPLAIN'] . '
+
' . (($this->config_generate()) ? '' : $this->user->lang('FULLTEXT_SPHINX_NO_CONFIG_DATA')) . '
+
'; // These are fields required in the config table diff --git a/phpBB/language/en/acp/search.php b/phpBB/language/en/acp/search.php index 9618bb90c5..778f9ddec5 100644 --- a/phpBB/language/en/acp/search.php +++ b/phpBB/language/en/acp/search.php @@ -85,6 +85,9 @@ $lang = array_merge($lang, array( 'FULLTEXT_SPHINX_WRONG_DATABASE' => 'The sphinx plugin for phpBB currently only supports MySQL', 'FULLTEXT_SPHINX_STOPWORDS_FILE' => 'Stopwords activated', 'FULLTEXT_SPHINX_STOPWORDS_FILE_EXPLAIN' => 'This setting only works with autoconf enabled. You can place a file called sphinx_stopwords.txt containing one word in each line in your config directory. If this file is present these words will be excluded from the indexing process.', + 'FULLTEXT_SPHINX_CONFIG_FILE' => 'Sphinx config file', + 'FULLTEXT_SPHINX_CONFIG_FILE_EXPLAIN' => 'The generated content of the sphinx config file. This data needs to be pasted into the sphinx.conf which is used by sphinx search daemon.', + 'FULLTEXT_SPHINX_NO_CONFIG_DATA' => 'The sphinx data and config directory paths are not defined. Please define them to generate the config file.', 'GENERAL_SEARCH_SETTINGS' => 'General search settings', 'GO_TO_SEARCH_INDEX' => 'Go to search index page', -- cgit v1.2.1 From 172c583f1941a8b162f1a7bf258bb3e38149606d Mon Sep 17 00:00:00 2001 From: Dhruv Date: Wed, 11 Jul 2012 16:57:18 +0530 Subject: [feature/sphinx-fulltext-search] use new unique id instead of salt a new unique id is generated by sphinx and stored in the config table instead of using avatar_salt. PHPBB3-10946 --- phpBB/docs/sphinx.sample.conf | 16 ++++++++-------- phpBB/includes/search/fulltext_sphinx.php | 15 ++++++++------- 2 files changed, 16 insertions(+), 15 deletions(-) (limited to 'phpBB') diff --git a/phpBB/docs/sphinx.sample.conf b/phpBB/docs/sphinx.sample.conf index 000d8157d6..06595a766f 100644 --- a/phpBB/docs/sphinx.sample.conf +++ b/phpBB/docs/sphinx.sample.conf @@ -1,4 +1,4 @@ -source source_phpbb_{AVATAR_SALT}_main +source source_phpbb_{SPHINX_ID}_main { type = mysql sql_host = localhost @@ -38,7 +38,7 @@ source source_phpbb_{AVATAR_SALT}_main sql_attr_timestamp = topic_last_post_time sql_attr_str2ordinal = post_subject } -source source_phpbb_{AVATAR_SALT}_delta : source_phpbb_{AVATAR_SALT}_main +source source_phpbb_{SPHINX_ID}_delta : source_phpbb_{SPHINX_ID}_main { sql_query_range = sql_range_step = @@ -60,10 +60,10 @@ source source_phpbb_{AVATAR_SALT}_delta : source_phpbb_{AVATAR_SALT}_main AND p.post_id >= ( SELECT max_doc_id FROM phpbb_sphinx WHERE counter_id=1 ) sql_query_pre = } -index index_phpbb_{AVATAR_SALT}_main +index index_phpbb_{SPHINX_ID}_main { - path = {DATA_PATH}/index_phpbb_{AVATAR_SALT}_main - source = source_phpbb_{AVATAR_SALT}_main + path = {DATA_PATH}/index_phpbb_{SPHINX_ID}_main + source = source_phpbb_{SPHINX_ID}_main docinfo = extern morphology = none stopwords = @@ -73,10 +73,10 @@ index index_phpbb_{AVATAR_SALT}_main min_prefix_len = 0 min_infix_len = 0 } -index index_phpbb_{AVATAR_SALT}_delta : index_phpbb_{AVATAR_SALT}_main +index index_phpbb_{SPHINX_ID}_delta : index_phpbb_{SPHINX_ID}_main { - path = {DATA_PATH}/index_phpbb_{AVATAR_SALT}_delta - source = source_phpbb_{AVATAR_SALT}_delta + path = {DATA_PATH}/index_phpbb_{SPHINX_ID}_delta + source = source_phpbb_{SPHINX_ID}_delta } indexer { diff --git a/phpBB/includes/search/fulltext_sphinx.php b/phpBB/includes/search/fulltext_sphinx.php index 6e554eec00..5bdbbff119 100644 --- a/phpBB/includes/search/fulltext_sphinx.php +++ b/phpBB/includes/search/fulltext_sphinx.php @@ -72,16 +72,17 @@ class phpbb_search_fulltext_sphinx // Initialize phpbb_db_tools object $this->db_tools = new phpbb_db_tools($this->db); - $this->id = $config['avatar_salt']; + if(!$this->config['fulltext_sphinx_id']) + { + set_config('fulltext_sphinx_id', unique_id()); + } + $this->id = $this->config['fulltext_sphinx_id']; $this->indexes = 'index_phpbb_' . $this->id . '_delta;index_phpbb_' . $this->id . '_main'; $this->sphinx = new SphinxClient(); // We only support localhost for now - $this->sphinx->SetServer('localhost', (isset($config['fulltext_sphinx_port']) && $config['fulltext_sphinx_port']) ? (int) $config['fulltext_sphinx_port'] : 3312); - - $config['fulltext_sphinx_min_word_len'] = 2; - $config['fulltext_sphinx_max_word_len'] = 400; + $this->sphinx->SetServer('localhost', (isset($this->config['fulltext_sphinx_port']) && $this->config['fulltext_sphinx_port']) ? (int) $this->config['fulltext_sphinx_port'] : 3312); $error = false; } @@ -137,8 +138,8 @@ class phpbb_search_fulltext_sphinx include ($phpbb_root_path . 'config.' . $phpEx); /* Now that we're sure everything was entered correctly, - generate a config for the index. We misuse the avatar_salt - for this, as it should be unique. */ + generate a config for the index. We use a config value + fulltext_sphinx_id for this, as it should be unique. */ $config_object = new phpbb_search_sphinx_config($this->config_file_data); $config_data = array( -- cgit v1.2.1 From b8103c5c31cbb42a46a40ac10c34ff09dc5efc60 Mon Sep 17 00:00:00 2001 From: Dhruv Date: Wed, 11 Jul 2012 17:10:51 +0530 Subject: [feature/sphinx-fulltext-search] fix comments and indentation PHPBB3-10946 --- phpBB/includes/search/fulltext_sphinx.php | 2 +- phpBB/includes/search/sphinx/config.php | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) (limited to 'phpBB') diff --git a/phpBB/includes/search/fulltext_sphinx.php b/phpBB/includes/search/fulltext_sphinx.php index 5bdbbff119..d942d0f027 100644 --- a/phpBB/includes/search/fulltext_sphinx.php +++ b/phpBB/includes/search/fulltext_sphinx.php @@ -135,7 +135,7 @@ class phpbb_search_fulltext_sphinx return false; } - include ($phpbb_root_path . 'config.' . $phpEx); + include($phpbb_root_path . 'config.' . $phpEx); /* Now that we're sure everything was entered correctly, generate a config for the index. We use a config value diff --git a/phpBB/includes/search/sphinx/config.php b/phpBB/includes/search/sphinx/config.php index e0ad667fb6..173193adc5 100644 --- a/phpBB/includes/search/sphinx/config.php +++ b/phpBB/includes/search/sphinx/config.php @@ -98,7 +98,7 @@ class phpbb_search_sphinx_config // If we're not inside a section look for one if (!$section) { - /* add empty lines and comments as comment objects to the section list + /* Add empty lines and comments as comment objects to the section list that way they're not deleted when reassembling the file from the sections*/ if (!$line || $line[0] == '#') { @@ -107,7 +107,7 @@ class phpbb_search_sphinx_config } else { - /* otherwise we scan the line reading the section name until we find + /* Otherwise we scan the line reading the section name until we find an opening curly bracket or a comment */ $section_name = ''; $section_name_comment = ''; @@ -147,7 +147,7 @@ class phpbb_search_sphinx_config // If we're not in a value continuing over the line feed if (!$in_value) { - /* then add empty lines and comments as comment objects to the variable list + /* Then add empty lines and comments as comment objects to the variable list of this section so they're not deleted on reassembly */ if (!$line || $line[0] == '#') { @@ -240,7 +240,7 @@ class phpbb_search_sphinx_config continue; } - /* if we found a closing curly bracket this section has been completed + /* If we found a closing curly bracket this section has been completed and we can append it to the section list and continue with looking for the next section */ if ($end_section) -- cgit v1.2.1 From 78e7f2a5290dc152cf2e386553e6308c74e2d005 Mon Sep 17 00:00:00 2001 From: Dhruv Date: Wed, 11 Jul 2012 17:32:31 +0530 Subject: [feature/sphinx-fulltext-search] improve sphinx helper classes add access modifiers and docblocks to properties and methods of sphinx helper classes. PHPBB3-10946 --- phpBB/includes/search/sphinx/config.php | 15 +++++++++++--- phpBB/includes/search/sphinx/config_comment.php | 6 +++++- phpBB/includes/search/sphinx/config_section.php | 26 ++++++++++++++++++++---- phpBB/includes/search/sphinx/config_variable.php | 14 ++++++++++--- 4 files changed, 50 insertions(+), 11 deletions(-) (limited to 'phpBB') diff --git a/phpBB/includes/search/sphinx/config.php b/phpBB/includes/search/sphinx/config.php index 173193adc5..795dff07ed 100644 --- a/phpBB/includes/search/sphinx/config.php +++ b/phpBB/includes/search/sphinx/config.php @@ -23,13 +23,14 @@ if (!defined('IN_PHPBB')) */ class phpbb_search_sphinx_config { - var $loaded = false; - var $sections = array(); + private $sections = array(); /** * Constructor which optionally loads data from a variable * * @param string $config_data Variable containing the sphinx configuration data + * + * @access public */ function __construct($config_data) { @@ -44,6 +45,8 @@ class phpbb_search_sphinx_config * * @param string $name The name of the section that shall be returned * @return phpbb_search_sphinx_config_section The section object or null if none was found + * + * @access public */ function get_section_by_name($name) { @@ -62,6 +65,8 @@ class phpbb_search_sphinx_config * * @param string $name The name for the new section * @return phpbb_search_sphinx_config_section The newly created section object + * + * @access public */ function add_section($name) { @@ -73,6 +78,8 @@ class phpbb_search_sphinx_config * Reads the config file data * * @param string $config_data The config file data + * + * @access private */ function read($config_data) { @@ -264,7 +271,9 @@ class phpbb_search_sphinx_config /** * Returns the config data * - * @return string $data The config data that is generated. + * @return string $data The config data that is generated + * + * @access public */ function get_data() { diff --git a/phpBB/includes/search/sphinx/config_comment.php b/phpBB/includes/search/sphinx/config_comment.php index 63d3488aef..7f695dbf0c 100644 --- a/phpBB/includes/search/sphinx/config_comment.php +++ b/phpBB/includes/search/sphinx/config_comment.php @@ -21,12 +21,14 @@ if (!defined('IN_PHPBB')) */ class phpbb_search_sphinx_config_comment { - var $exact_string; + private $exact_string; /** * Create a new comment * * @param string $exact_string The content of the comment including newlines, leading whitespace, etc. + * + * @access public */ function __construct($exact_string) { @@ -37,6 +39,8 @@ class phpbb_search_sphinx_config_comment * Simply returns the comment as it was created * * @return string The exact string that was specified in the constructor + * + * @access public */ function to_string() { diff --git a/phpBB/includes/search/sphinx/config_section.php b/phpBB/includes/search/sphinx/config_section.php index ed20dba279..79c9c8563d 100644 --- a/phpBB/includes/search/sphinx/config_section.php +++ b/phpBB/includes/search/sphinx/config_section.php @@ -21,10 +21,10 @@ if (!defined('IN_PHPBB')) */ class phpbb_search_sphinx_config_section { - var $name; - var $comment; - var $end_comment; - var $variables = array(); + private $name; + private $comment; + private $end_comment; + private $variables = array(); /** * Construct a new section @@ -32,6 +32,8 @@ class phpbb_search_sphinx_config_section * @param string $name Name of the section * @param string $comment Comment that should be appended after the name in the * textual format. + * + * @access public */ function __construct($name, $comment) { @@ -44,6 +46,8 @@ class phpbb_search_sphinx_config_section * Add a variable object to the list of variables in this section * * @param phpbb_search_sphinx_config_variable $variable The variable object + * + * @access public */ function add_variable($variable) { @@ -52,6 +56,10 @@ class phpbb_search_sphinx_config_section /** * Adds a comment after the closing bracket in the textual representation + * + * @param string $end_comment + * + * @access public */ function set_end_comment($end_comment) { @@ -62,6 +70,8 @@ class phpbb_search_sphinx_config_section * Getter for the name of this section * * @return string Section's name + * + * @access public */ function get_name() { @@ -74,6 +84,8 @@ class phpbb_search_sphinx_config_section * @param string $name The name of the variable that shall be returned * @return phpbb_search_sphinx_config_section The first variable object from this section with the * given name or null if none was found + * + * @access public */ function get_variable_by_name($name) { @@ -91,6 +103,8 @@ class phpbb_search_sphinx_config_section * Deletes all variables with the given name * * @param string $name The name of the variable objects that are supposed to be removed + * + * @access public */ function delete_variables_by_name($name) { @@ -111,6 +125,8 @@ class phpbb_search_sphinx_config_section * @param string $name The name for the new variable * @param string $value The value for the new variable * @return phpbb_search_sphinx_config_variable Variable object that was created + * + * @access public */ function create_variable($name, $value) { @@ -122,6 +138,8 @@ class phpbb_search_sphinx_config_section * Turns this object into a string which can be written to a config file * * @return string Config data in textual form, parsable for sphinx + * + * @access public */ function to_string() { diff --git a/phpBB/includes/search/sphinx/config_variable.php b/phpBB/includes/search/sphinx/config_variable.php index dd7836f7c8..35abe281cb 100644 --- a/phpBB/includes/search/sphinx/config_variable.php +++ b/phpBB/includes/search/sphinx/config_variable.php @@ -21,9 +21,9 @@ if (!defined('IN_PHPBB')) */ class phpbb_search_sphinx_config_variable { - var $name; - var $value; - var $comment; + private $name; + private $value; + private $comment; /** * Constructs a new variable object @@ -32,6 +32,8 @@ class phpbb_search_sphinx_config_variable * @param string $value Value of the variable * @param string $comment Optional comment after the variable in the * config file + * + * @access public */ function __construct($name, $value, $comment) { @@ -44,6 +46,8 @@ class phpbb_search_sphinx_config_variable * Getter for the variable's name * * @return string The variable object's name + * + * @access public */ function get_name() { @@ -54,6 +58,8 @@ class phpbb_search_sphinx_config_variable * Allows changing the variable's value * * @param string $value New value for this variable + * + * @access public */ function set_value($value) { @@ -64,6 +70,8 @@ class phpbb_search_sphinx_config_variable * Turns this object into a string readable by sphinx * * @return string Config data in textual form + * + * @access public */ function to_string() { -- cgit v1.2.1 From f40da411c389cb7718d31f1ee20f8487f25969f0 Mon Sep 17 00:00:00 2001 From: Dhruv Date: Wed, 11 Jul 2012 17:46:58 +0530 Subject: [feature/sphinx-fulltext-search] modify language keys Modify language keys according to what the config setting actually does. Remove references to autoconf. PHPBB3-10946 --- phpBB/includes/search/fulltext_sphinx.php | 3 +-- phpBB/language/en/acp/search.php | 10 ++++------ 2 files changed, 5 insertions(+), 8 deletions(-) (limited to 'phpBB') diff --git a/phpBB/includes/search/fulltext_sphinx.php b/phpBB/includes/search/fulltext_sphinx.php index d942d0f027..08948803ba 100644 --- a/phpBB/includes/search/fulltext_sphinx.php +++ b/phpBB/includes/search/fulltext_sphinx.php @@ -746,7 +746,7 @@ class phpbb_search_fulltext_sphinx ); $tpl = ' - ' . $this->user->lang['FULLTEXT_SPHINX_CONFIGURE_BEFORE']. ' + ' . $this->user->lang['FULLTEXT_SPHINX_CONFIGURE']. '

' . $this->user->lang['FULLTEXT_SPHINX_CONFIG_PATH_EXPLAIN'] . '
@@ -759,7 +759,6 @@ class phpbb_search_fulltext_sphinx

' . $this->user->lang['FULLTEXT_SPHINX_STOPWORDS_FILE_EXPLAIN'] . '
- ' . $this->user->lang['FULLTEXT_SPHINX_CONFIGURE_AFTER']. '

' . $this->user->lang['FULLTEXT_SPHINX_PORT_EXPLAIN'] . '
diff --git a/phpBB/language/en/acp/search.php b/phpBB/language/en/acp/search.php index 778f9ddec5..970d9cd41b 100644 --- a/phpBB/language/en/acp/search.php +++ b/phpBB/language/en/acp/search.php @@ -70,12 +70,10 @@ $lang = array_merge($lang, array( 'FULLTEXT_POSTGRES_MAX_WORD_LEN_EXPLAIN' => 'Words with no more than this many characters will be included in the query to the database.', 'FULLTEXT_SPHINX_CONFIG_PATH' => 'Path to configuration directory', - 'FULLTEXT_SPHINX_CONFIG_PATH_EXPLAIN' => 'Skip if autoconf is disabled. You should create this config directory outside the web accessable directories. It has to be writable by the user as which your webserver is running (often www-data or nobody).', - 'FULLTEXT_SPHINX_CONFIGURE_FIRST' => 'Before you create an index you have to enable and configure sphinx under GENERAL -> SERVER CONFIGURATION -> Search settings.', - 'FULLTEXT_SPHINX_CONFIGURE_BEFORE' => 'Configure the following settings BEFORE activating Sphinx', - 'FULLTEXT_SPHINX_CONFIGURE_AFTER' => 'The following settings do not have to be configured before activating Sphinx', + 'FULLTEXT_SPHINX_CONFIG_PATH_EXPLAIN' => 'You should put the sphinx.conf file in this directory. This config directory should be outside the web accessable directories.', + 'FULLTEXT_SPHINX_CONFIGURE' => 'Configure the following settings to generate sphinx config file', 'FULLTEXT_SPHINX_DATA_PATH' => 'Path to data directory', - 'FULLTEXT_SPHINX_DATA_PATH_EXPLAIN' => 'Skip if autorun is disabled. You should create this directory outside the web accessable directories. It has to be writable by the user as which your webserver is running (often www-data or nobody). It will be used to store the indexes and log files.', + 'FULLTEXT_SPHINX_DATA_PATH_EXPLAIN' => 'It will be used to store the indexes and log files. You should create this directory outside the web accessable directories.', 'FULLTEXT_SPHINX_DELTA_POSTS' => 'Number of posts in frequently updated delta index', 'FULLTEXT_SPHINX_INDEXER_MEM_LIMIT' => 'Indexer memory limit', 'FULLTEXT_SPHINX_INDEXER_MEM_LIMIT_EXPLAIN' => 'This number should at all times be lower than the RAM available on your machine. If you experience periodic performance problems this might be due to the indexer consuming too many resources. It might help to lower the amount of memory available to the indexer.', @@ -84,7 +82,7 @@ $lang = array_merge($lang, array( 'FULLTEXT_SPHINX_PORT_EXPLAIN' => 'Port on which the sphinx search deamon on localhost listens. Leave empty to use the default 3312', 'FULLTEXT_SPHINX_WRONG_DATABASE' => 'The sphinx plugin for phpBB currently only supports MySQL', 'FULLTEXT_SPHINX_STOPWORDS_FILE' => 'Stopwords activated', - 'FULLTEXT_SPHINX_STOPWORDS_FILE_EXPLAIN' => 'This setting only works with autoconf enabled. You can place a file called sphinx_stopwords.txt containing one word in each line in your config directory. If this file is present these words will be excluded from the indexing process.', + 'FULLTEXT_SPHINX_STOPWORDS_FILE_EXPLAIN' => 'You can place a file called sphinx_stopwords.txt containing one word in each line in your config directory. If this file is present these words will be excluded from the indexing process.', 'FULLTEXT_SPHINX_CONFIG_FILE' => 'Sphinx config file', 'FULLTEXT_SPHINX_CONFIG_FILE_EXPLAIN' => 'The generated content of the sphinx config file. This data needs to be pasted into the sphinx.conf which is used by sphinx search daemon.', 'FULLTEXT_SPHINX_NO_CONFIG_DATA' => 'The sphinx data and config directory paths are not defined. Please define them to generate the config file.', -- cgit v1.2.1 From 13c451ca2e9a6717a8a98943ba022a6f41dcdd9c Mon Sep 17 00:00:00 2001 From: Dhruv Date: Thu, 12 Jul 2012 04:13:34 +0530 Subject: [feature/sphinx-fulltext-search] use sql_table_exists Use sql_table_exists( ) method in db_tools to support all database types. PHPBB3-10946 --- phpBB/includes/search/fulltext_sphinx.php | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) (limited to 'phpBB') diff --git a/phpBB/includes/search/fulltext_sphinx.php b/phpBB/includes/search/fulltext_sphinx.php index 08948803ba..747c22a3ef 100644 --- a/phpBB/includes/search/fulltext_sphinx.php +++ b/phpBB/includes/search/fulltext_sphinx.php @@ -667,14 +667,9 @@ class phpbb_search_fulltext_sphinx */ function index_created($allow_new_files = true) { - $sql = 'SHOW TABLES LIKE \'' . SPHINX_TABLE . '\''; - $result = $this->db->sql_query($sql); - $row = $this->db->sql_fetchrow($result); - $this->db->sql_freeresult($result); - $created = false; - if ($row) + if ($this->db_tools->sql_table_exists(SPHINX_TABLE)) { $created = true; } -- cgit v1.2.1 From 118b57f71d9f563f8eeda8e5925d482c38ab1af8 Mon Sep 17 00:00:00 2001 From: Dhruv Date: Thu, 12 Jul 2012 04:28:55 +0530 Subject: [feature/sphinx-fulltext-search] minor changes in sphinx.conf PHPBB3-10946 --- phpBB/docs/sphinx.sample.conf | 4 ++-- phpBB/includes/search/fulltext_sphinx.php | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) (limited to 'phpBB') diff --git a/phpBB/docs/sphinx.sample.conf b/phpBB/docs/sphinx.sample.conf index 06595a766f..eaef081aa1 100644 --- a/phpBB/docs/sphinx.sample.conf +++ b/phpBB/docs/sphinx.sample.conf @@ -6,6 +6,8 @@ source source_phpbb_{SPHINX_ID}_main sql_pass = password sql_db = db_name sql_port = 3306 #optional, default is 3306 + sql_query_pre = SET NAMES 'utf8' + sql_query_pre = REPLACE INTO phpbb_sphinx SELECT 1, MAX(post_id) FROM phpbb_posts sql_query_range = SELECT MIN(post_id), MAX(post_id) FROM phpbb_posts sql_range_step = 5000 sql_query = SELECT \ @@ -27,8 +29,6 @@ source source_phpbb_{SPHINX_ID}_main sql_query_post = sql_query_post_index = REPLACE INTO phpbb_sphinx ( counter_id, max_doc_id ) VALUES ( 1, $maxid ) sql_query_info = SELECT * FROM phpbb_posts WHERE post_id = $id - sql_query_pre = SET NAMES utf8 - sql_query_pre = REPLACE INTO phpbb_sphinx SELECT 1, MAX(post_id) FROM phpbb_posts sql_attr_uint = forum_id sql_attr_uint = topic_id sql_attr_uint = poster_id diff --git a/phpBB/includes/search/fulltext_sphinx.php b/phpBB/includes/search/fulltext_sphinx.php index 747c22a3ef..c223284c72 100644 --- a/phpBB/includes/search/fulltext_sphinx.php +++ b/phpBB/includes/search/fulltext_sphinx.php @@ -150,7 +150,7 @@ class phpbb_search_fulltext_sphinx array('sql_pass', $dbpasswd), array('sql_db', $dbname), array('sql_port', $dbport), - array('sql_query_pre', 'SET NAMES utf8'), + array('sql_query_pre', 'SET NAMES \'utf8\''), array('sql_query_pre', 'REPLACE INTO ' . SPHINX_TABLE . ' SELECT 1, MAX(post_id) FROM ' . POSTS_TABLE . ''), array('sql_query_range', 'SELECT MIN(post_id), MAX(post_id) FROM ' . POSTS_TABLE . ''), array('sql_range_step', '5000'), -- cgit v1.2.1 From b81941a997760eca4f209cc100fe2baec3ef4468 Mon Sep 17 00:00:00 2001 From: Dhruv Date: Thu, 12 Jul 2012 16:30:45 +0530 Subject: [feature/sphinx-fulltext-search] use CASE instead of IF IF is not supported in pgsql, use CASE instead supported in both mysql and pgsql. PHPBB3-10946 --- phpBB/docs/sphinx.sample.conf | 4 ++-- phpBB/includes/search/fulltext_sphinx.php | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) (limited to 'phpBB') diff --git a/phpBB/docs/sphinx.sample.conf b/phpBB/docs/sphinx.sample.conf index eaef081aa1..3ab2552096 100644 --- a/phpBB/docs/sphinx.sample.conf +++ b/phpBB/docs/sphinx.sample.conf @@ -15,7 +15,7 @@ source source_phpbb_{SPHINX_ID}_main p.forum_id, \ p.topic_id, \ p.poster_id, \ - IF(p.post_id = t.topic_first_post_id, 1, 0) as topic_first_post, \ + CASE WHEN p.post_id = t.topic_first_post_id THEN 1 ELSE 0 END as topic_first_post, \ p.post_time, \ p.post_subject, \ p.post_subject as title, \ @@ -47,7 +47,7 @@ source source_phpbb_{SPHINX_ID}_delta : source_phpbb_{SPHINX_ID}_main p.forum_id, \ p.topic_id, \ p.poster_id, \ - IF(p.post_id = t.topic_first_post_id, 1, 0) as topic_first_post, \ + CASE WHEN p.post_id = t.topic_first_post_id THEN 1 ELSE 0 END as topic_first_post, \ p.post_time, \ p.post_subject, \ p.post_subject as title, \ diff --git a/phpBB/includes/search/fulltext_sphinx.php b/phpBB/includes/search/fulltext_sphinx.php index c223284c72..f505703c09 100644 --- a/phpBB/includes/search/fulltext_sphinx.php +++ b/phpBB/includes/search/fulltext_sphinx.php @@ -159,7 +159,7 @@ class phpbb_search_fulltext_sphinx p.forum_id, p.topic_id, p.poster_id, - IF(p.post_id = t.topic_first_post_id, 1, 0) as topic_first_post, + CASE WHEN p.post_id = t.topic_first_post_id THEN 1 ELSE 0 END as topic_first_post, p.post_time, p.post_subject, p.post_subject as title, @@ -191,7 +191,7 @@ class phpbb_search_fulltext_sphinx p.forum_id, p.topic_id, p.poster_id, - IF(p.post_id = t.topic_first_post_id, 1, 0) as topic_first_post, + CASE WHEN p.post_id = t.topic_first_post_id THEN 1 ELSE 0 END as topic_first_post, p.post_time, p.post_subject, p.post_subject as title, -- cgit v1.2.1 From 81959927e53ebc62765ff075d23feeaf9b40a95d Mon Sep 17 00:00:00 2001 From: Dhruv Date: Thu, 12 Jul 2012 17:22:03 +0530 Subject: [feature/sphinx-fulltext-search] use Update in sphinx query Instead of REPLACE use UPDATE since pgsql does not support REPLACE. A row is inserted at time of creating table so REPLACE is no longer needed. PHPBB3-10946 --- phpBB/docs/sphinx.sample.conf | 4 ++-- phpBB/includes/search/fulltext_sphinx.php | 11 +++++++++-- 2 files changed, 11 insertions(+), 4 deletions(-) (limited to 'phpBB') diff --git a/phpBB/docs/sphinx.sample.conf b/phpBB/docs/sphinx.sample.conf index 3ab2552096..8ffd54a880 100644 --- a/phpBB/docs/sphinx.sample.conf +++ b/phpBB/docs/sphinx.sample.conf @@ -7,7 +7,7 @@ source source_phpbb_{SPHINX_ID}_main sql_db = db_name sql_port = 3306 #optional, default is 3306 sql_query_pre = SET NAMES 'utf8' - sql_query_pre = REPLACE INTO phpbb_sphinx SELECT 1, MAX(post_id) FROM phpbb_posts + sql_query_pre = UPDATE phpbb_sphinx SET max_doc_id = MAX(post_id) WHERE counter_id = 1 sql_query_range = SELECT MIN(post_id), MAX(post_id) FROM phpbb_posts sql_range_step = 5000 sql_query = SELECT \ @@ -27,7 +27,7 @@ source source_phpbb_{SPHINX_ID}_main p.topic_id = t.topic_id \ AND p.post_id >= $start AND p.post_id <= $end sql_query_post = - sql_query_post_index = REPLACE INTO phpbb_sphinx ( counter_id, max_doc_id ) VALUES ( 1, $maxid ) + sql_query_post_index = UPDATE phpbb_sphinx SET max_doc_id = $maxid WHERE counter_id = 1 sql_query_info = SELECT * FROM phpbb_posts WHERE post_id = $id sql_attr_uint = forum_id sql_attr_uint = topic_id diff --git a/phpBB/includes/search/fulltext_sphinx.php b/phpBB/includes/search/fulltext_sphinx.php index f505703c09..d82a56c6f4 100644 --- a/phpBB/includes/search/fulltext_sphinx.php +++ b/phpBB/includes/search/fulltext_sphinx.php @@ -151,7 +151,7 @@ class phpbb_search_fulltext_sphinx array('sql_db', $dbname), array('sql_port', $dbport), array('sql_query_pre', 'SET NAMES \'utf8\''), - array('sql_query_pre', 'REPLACE INTO ' . SPHINX_TABLE . ' SELECT 1, MAX(post_id) FROM ' . POSTS_TABLE . ''), + array('sql_query_pre', 'UPDATE ' . SPHINX_TABLE . ' SET max_doc_id = (SELECT MAX(post_id) FROM ' . POSTS_TABLE . ') WHERE counter_id = 1'), array('sql_query_range', 'SELECT MIN(post_id), MAX(post_id) FROM ' . POSTS_TABLE . ''), array('sql_range_step', '5000'), array('sql_query', 'SELECT @@ -171,7 +171,7 @@ class phpbb_search_fulltext_sphinx p.topic_id = t.topic_id AND p.post_id >= $start AND p.post_id <= $end'), array('sql_query_post', ''), - array('sql_query_post_index', 'REPLACE INTO ' . SPHINX_TABLE . ' ( counter_id, max_doc_id ) VALUES ( 1, $maxid )'), + array('sql_query_post_index', 'UPDATE ' . SPHINX_TABLE . ' SET max_doc_id = $maxid WHERE counter_id = 1'), array('sql_query_info', 'SELECT * FROM ' . POSTS_TABLE . ' WHERE post_id = $id'), array('sql_attr_uint', 'forum_id'), array('sql_attr_uint', 'topic_id'), @@ -634,6 +634,13 @@ class phpbb_search_fulltext_sphinx $sql = 'TRUNCATE TABLE ' . SPHINX_TABLE; $this->db->sql_query($sql); + + $data = array( + 'counter_id' => '1', + 'max_doc_id' => '0', + ); + $sql = 'INSERT INTO ' . SPHINX_TABLE . ' ' . $this->db->sql_build_array('INSERT', $data); + $this->db->sql_query($sql); } return false; -- cgit v1.2.1 From 609ce3ae8fb55e717ff188d2ec9c10c6ae252b7a Mon Sep 17 00:00:00 2001 From: Dhruv Date: Thu, 12 Jul 2012 17:48:17 +0530 Subject: [feature/sphinx-fulltext-search] add pgsql functionality PHPBB3-10946 --- phpBB/includes/search/fulltext_sphinx.php | 2 +- phpBB/language/en/acp/search.php | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) (limited to 'phpBB') diff --git a/phpBB/includes/search/fulltext_sphinx.php b/phpBB/includes/search/fulltext_sphinx.php index d82a56c6f4..76caf9ae8c 100644 --- a/phpBB/includes/search/fulltext_sphinx.php +++ b/phpBB/includes/search/fulltext_sphinx.php @@ -108,7 +108,7 @@ class phpbb_search_fulltext_sphinx */ function init() { - if ($this->db->sql_layer != 'mysql' && $this->db->sql_layer != 'mysql4' && $this->db->sql_layer != 'mysqli') + if ($this->db->sql_layer != 'mysql' && $this->db->sql_layer != 'mysql4' && $this->db->sql_layer != 'mysqli' && $this->db->sql_layer != 'postgres') { return $this->user->lang['FULLTEXT_SPHINX_WRONG_DATABASE']; } diff --git a/phpBB/language/en/acp/search.php b/phpBB/language/en/acp/search.php index 970d9cd41b..4aa9c89cde 100644 --- a/phpBB/language/en/acp/search.php +++ b/phpBB/language/en/acp/search.php @@ -80,7 +80,7 @@ $lang = array_merge($lang, array( 'FULLTEXT_SPHINX_MAIN_POSTS' => 'Number of posts in main index', 'FULLTEXT_SPHINX_PORT' => 'Sphinx search deamon port', 'FULLTEXT_SPHINX_PORT_EXPLAIN' => 'Port on which the sphinx search deamon on localhost listens. Leave empty to use the default 3312', - 'FULLTEXT_SPHINX_WRONG_DATABASE' => 'The sphinx plugin for phpBB currently only supports MySQL', + 'FULLTEXT_SPHINX_WRONG_DATABASE' => 'The sphinx search for phpBB supports MySQL and PostgreSQL only.', 'FULLTEXT_SPHINX_STOPWORDS_FILE' => 'Stopwords activated', 'FULLTEXT_SPHINX_STOPWORDS_FILE_EXPLAIN' => 'You can place a file called sphinx_stopwords.txt containing one word in each line in your config directory. If this file is present these words will be excluded from the indexing process.', 'FULLTEXT_SPHINX_CONFIG_FILE' => 'Sphinx config file', -- cgit v1.2.1 From a3d103c9c03c79fe67963b9db5a5471c766fa401 Mon Sep 17 00:00:00 2001 From: Dhruv Date: Thu, 12 Jul 2012 18:08:50 +0530 Subject: [feature/sphinx-fulltext-search] add support for postgres Don't generate sphinx config file if database is not supported. Add property $dbtype to write into sphinx config file according to sql_layer. PHPBB3-10946 --- phpBB/includes/search/fulltext_sphinx.php | 23 ++++++++++++++++++++--- 1 file changed, 20 insertions(+), 3 deletions(-) (limited to 'phpBB') diff --git a/phpBB/includes/search/fulltext_sphinx.php b/phpBB/includes/search/fulltext_sphinx.php index 76caf9ae8c..a4341f46e0 100644 --- a/phpBB/includes/search/fulltext_sphinx.php +++ b/phpBB/includes/search/fulltext_sphinx.php @@ -44,6 +44,7 @@ class phpbb_search_fulltext_sphinx private $config; private $db; private $db_tools; + private $dbtype; private $user; private $config_file_data = ''; public $word_length = array(); @@ -130,8 +131,25 @@ class phpbb_search_fulltext_sphinx { global $phpbb_root_path, $phpEx; + // Check if Database is supported by Sphinx + if ($this->db->sql_layer =='mysql' || $this->db->sql_layer == 'mysql4' || $this->db->sql_layer == 'mysqli') + { + $this->dbtype = 'mysql'; + } + else if ($this->db->sql_layer == 'postgres') + { + $this->dbtype = 'pgsql'; + } + else + { + $this->config_file_data = $this->user->lang('FULLTEXT_SPHINX_WRONG_DATABASE'); + return false; + } + + // Check if directory paths have been filled if (!$this->config['fulltext_sphinx_data_path'] || !$this->config['fulltext_sphinx_config_path']) { + $this->config_file_data = $this->user->lang('FULLTEXT_SPHINX_NO_CONFIG_DATA'); return false; } @@ -141,10 +159,9 @@ class phpbb_search_fulltext_sphinx generate a config for the index. We use a config value fulltext_sphinx_id for this, as it should be unique. */ $config_object = new phpbb_search_sphinx_config($this->config_file_data); - $config_data = array( 'source source_phpbb_' . $this->id . '_main' => array( - array('type', 'mysql'), + array('type', $this->dbtype), array('sql_host', $dbhost), array('sql_user', $dbuser), array('sql_pass', $dbpasswd), @@ -771,7 +788,7 @@ class phpbb_search_fulltext_sphinx

' . $this->user->lang['FULLTEXT_SPHINX_CONFIG_FILE_EXPLAIN'] . '
-
' . (($this->config_generate()) ? '' : $this->user->lang('FULLTEXT_SPHINX_NO_CONFIG_DATA')) . '
+
' . (($this->config_generate()) ? '' : $this->config_file_data) . '
'; -- cgit v1.2.1 From 3ecc81f853bb1ec6262fc0615bb0ab8704616db9 Mon Sep 17 00:00:00 2001 From: Dhruv Date: Sat, 21 Jul 2012 14:14:19 +0530 Subject: [feature/sphinx-fulltext-search] remove note from db_tools Note saying db_tools not being used currently is remove from db_tools.php We utilize db_tools in sphinx search. PHPBB3-10946 --- phpBB/includes/db/db_tools.php | 1 - 1 file changed, 1 deletion(-) (limited to 'phpBB') diff --git a/phpBB/includes/db/db_tools.php b/phpBB/includes/db/db_tools.php index 73eae4e967..6df3aac9ce 100644 --- a/phpBB/includes/db/db_tools.php +++ b/phpBB/includes/db/db_tools.php @@ -20,7 +20,6 @@ if (!defined('IN_PHPBB')) * Currently not supported is returning SQL for creating tables. * * @package dbal -* @note currently not used within phpBB3, but may be utilized later. */ class phpbb_db_tools { -- cgit v1.2.1 From 1f77b95fe71e727238212ea4632220ae9cab99d7 Mon Sep 17 00:00:00 2001 From: Dhruv Date: Sun, 22 Jul 2012 01:49:30 +0530 Subject: [feature/sphinx-fulltext-search] fix language keys' typo PHPBB3-10946 --- phpBB/language/en/acp/search.php | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) (limited to 'phpBB') diff --git a/phpBB/language/en/acp/search.php b/phpBB/language/en/acp/search.php index 4aa9c89cde..37c403f43d 100644 --- a/phpBB/language/en/acp/search.php +++ b/phpBB/language/en/acp/search.php @@ -69,8 +69,8 @@ $lang = array_merge($lang, array( 'FULLTEXT_POSTGRES_MIN_WORD_LEN_EXPLAIN' => 'Words with at least this many characters will be included in the query to the database.', 'FULLTEXT_POSTGRES_MAX_WORD_LEN_EXPLAIN' => 'Words with no more than this many characters will be included in the query to the database.', - 'FULLTEXT_SPHINX_CONFIG_PATH' => 'Path to configuration directory', - 'FULLTEXT_SPHINX_CONFIG_PATH_EXPLAIN' => 'You should put the sphinx.conf file in this directory. This config directory should be outside the web accessable directories.', + 'FULLTEXT_SPHINX_CONFIG_PATH' => 'Path to config directory', + 'FULLTEXT_SPHINX_CONFIG_PATH_EXPLAIN' => 'You should put the sphinx.conf file in this directory. This config directory should be outside the web accessible directories.', 'FULLTEXT_SPHINX_CONFIGURE' => 'Configure the following settings to generate sphinx config file', 'FULLTEXT_SPHINX_DATA_PATH' => 'Path to data directory', 'FULLTEXT_SPHINX_DATA_PATH_EXPLAIN' => 'It will be used to store the indexes and log files. You should create this directory outside the web accessable directories.', @@ -78,8 +78,8 @@ $lang = array_merge($lang, array( 'FULLTEXT_SPHINX_INDEXER_MEM_LIMIT' => 'Indexer memory limit', 'FULLTEXT_SPHINX_INDEXER_MEM_LIMIT_EXPLAIN' => 'This number should at all times be lower than the RAM available on your machine. If you experience periodic performance problems this might be due to the indexer consuming too many resources. It might help to lower the amount of memory available to the indexer.', 'FULLTEXT_SPHINX_MAIN_POSTS' => 'Number of posts in main index', - 'FULLTEXT_SPHINX_PORT' => 'Sphinx search deamon port', - 'FULLTEXT_SPHINX_PORT_EXPLAIN' => 'Port on which the sphinx search deamon on localhost listens. Leave empty to use the default 3312', + 'FULLTEXT_SPHINX_PORT' => 'Sphinx search daemon port', + 'FULLTEXT_SPHINX_PORT_EXPLAIN' => 'Port on which the sphinx search daemon (searchd) listens. Leave empty to use the default 3312', 'FULLTEXT_SPHINX_WRONG_DATABASE' => 'The sphinx search for phpBB supports MySQL and PostgreSQL only.', 'FULLTEXT_SPHINX_STOPWORDS_FILE' => 'Stopwords activated', 'FULLTEXT_SPHINX_STOPWORDS_FILE_EXPLAIN' => 'You can place a file called sphinx_stopwords.txt containing one word in each line in your config directory. If this file is present these words will be excluded from the indexing process.', -- cgit v1.2.1 From 0e9eb9401a38fab3139a1df33fa7e0903ccfb18f Mon Sep 17 00:00:00 2001 From: Dhruv Date: Sun, 22 Jul 2012 01:53:04 +0530 Subject: [feature/sphinx-fulltext-search] use readonly instead of disabled PHPBB3-10946 --- phpBB/includes/search/fulltext_sphinx.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'phpBB') diff --git a/phpBB/includes/search/fulltext_sphinx.php b/phpBB/includes/search/fulltext_sphinx.php index a4341f46e0..1888057db4 100644 --- a/phpBB/includes/search/fulltext_sphinx.php +++ b/phpBB/includes/search/fulltext_sphinx.php @@ -788,7 +788,7 @@ class phpbb_search_fulltext_sphinx

' . $this->user->lang['FULLTEXT_SPHINX_CONFIG_FILE_EXPLAIN'] . '
-
' . (($this->config_generate()) ? '' : $this->config_file_data) . '
+
' . (($this->config_generate()) ? '' : $this->config_file_data) . '
'; -- cgit v1.2.1 From 161e469b5a67b2911089ec0dfdb70bef355ed07e Mon Sep 17 00:00:00 2001 From: Dhruv Date: Sun, 22 Jul 2012 02:50:53 +0530 Subject: [feature/sphinx-fulltext-search] makes sql host configurable The SQL server host which sphinx connects to index the posts is now configurable via ACP. PHPBB3-10946 --- phpBB/docs/sphinx.sample.conf | 4 ++-- phpBB/includes/search/fulltext_sphinx.php | 9 +++++++-- phpBB/language/en/acp/search.php | 2 ++ 3 files changed, 11 insertions(+), 4 deletions(-) (limited to 'phpBB') diff --git a/phpBB/docs/sphinx.sample.conf b/phpBB/docs/sphinx.sample.conf index 8ffd54a880..d0a897e0cc 100644 --- a/phpBB/docs/sphinx.sample.conf +++ b/phpBB/docs/sphinx.sample.conf @@ -1,7 +1,7 @@ source source_phpbb_{SPHINX_ID}_main { - type = mysql - sql_host = localhost + type = mysql #mysql or pgsql + sql_host = localhost #SQL server host sphinx connects to sql_user = username sql_pass = password sql_db = db_name diff --git a/phpBB/includes/search/fulltext_sphinx.php b/phpBB/includes/search/fulltext_sphinx.php index 1888057db4..3bdce5dfb9 100644 --- a/phpBB/includes/search/fulltext_sphinx.php +++ b/phpBB/includes/search/fulltext_sphinx.php @@ -162,11 +162,11 @@ class phpbb_search_fulltext_sphinx $config_data = array( 'source source_phpbb_' . $this->id . '_main' => array( array('type', $this->dbtype), - array('sql_host', $dbhost), + array('sql_host', $this->config['fulltext_sphinx_host'] ? $this->config['fulltext_sphinx_host'] : $dbhost), array('sql_user', $dbuser), array('sql_pass', $dbpasswd), array('sql_db', $dbname), - array('sql_port', $dbport), + array('sql_port', $this->config['fulltext_sphinx_port']), array('sql_query_pre', 'SET NAMES \'utf8\''), array('sql_query_pre', 'UPDATE ' . SPHINX_TABLE . ' SET max_doc_id = (SELECT MAX(post_id) FROM ' . POSTS_TABLE . ') WHERE counter_id = 1'), array('sql_query_range', 'SELECT MIN(post_id), MAX(post_id) FROM ' . POSTS_TABLE . ''), @@ -759,6 +759,7 @@ class phpbb_search_fulltext_sphinx $config_vars = array( 'fulltext_sphinx_config_path' => 'string', 'fulltext_sphinx_data_path' => 'string', + 'fulltext_sphinx_host' => 'string', 'fulltext_sphinx_port' => 'int', 'fulltext_sphinx_stopwords' => 'bool', 'fulltext_sphinx_indexer_mem_limit' => 'int', @@ -778,6 +779,10 @@ class phpbb_search_fulltext_sphinx

' . $this->user->lang['FULLTEXT_SPHINX_STOPWORDS_FILE_EXPLAIN'] . '
+
+

' . $this->user->lang['FULLTEXT_SPHINX_HOST_EXPLAIN'] . '
+
+

' . $this->user->lang['FULLTEXT_SPHINX_PORT_EXPLAIN'] . '
diff --git a/phpBB/language/en/acp/search.php b/phpBB/language/en/acp/search.php index 37c403f43d..99fbbac07e 100644 --- a/phpBB/language/en/acp/search.php +++ b/phpBB/language/en/acp/search.php @@ -75,6 +75,8 @@ $lang = array_merge($lang, array( 'FULLTEXT_SPHINX_DATA_PATH' => 'Path to data directory', 'FULLTEXT_SPHINX_DATA_PATH_EXPLAIN' => 'It will be used to store the indexes and log files. You should create this directory outside the web accessable directories.', 'FULLTEXT_SPHINX_DELTA_POSTS' => 'Number of posts in frequently updated delta index', + 'FULLTEXT_SPHINX_HOST' => 'SQL server host', + 'FULLTEXT_SPHINX_HOST_EXPLAIN' => 'SQL server host, which the sphinx search daemon (searchd) connects to. Leave empty to use the default SQL server host', 'FULLTEXT_SPHINX_INDEXER_MEM_LIMIT' => 'Indexer memory limit', 'FULLTEXT_SPHINX_INDEXER_MEM_LIMIT_EXPLAIN' => 'This number should at all times be lower than the RAM available on your machine. If you experience periodic performance problems this might be due to the indexer consuming too many resources. It might help to lower the amount of memory available to the indexer.', 'FULLTEXT_SPHINX_MAIN_POSTS' => 'Number of posts in main index', -- cgit v1.2.1 From e40758db84a23bc7a2c5324dbedcd7f0911abeea Mon Sep 17 00:00:00 2001 From: Dhruv Date: Sun, 22 Jul 2012 03:16:03 +0530 Subject: [feature/sphinx-fulltext-search] remove stopwords and config path Remove stopwords and config_path options from ACP. PHPBB3-10946 --- phpBB/includes/search/fulltext_sphinx.php | 14 ++------------ phpBB/language/en/acp/search.php | 4 ---- 2 files changed, 2 insertions(+), 16 deletions(-) (limited to 'phpBB') diff --git a/phpBB/includes/search/fulltext_sphinx.php b/phpBB/includes/search/fulltext_sphinx.php index 3bdce5dfb9..e1052ee7da 100644 --- a/phpBB/includes/search/fulltext_sphinx.php +++ b/phpBB/includes/search/fulltext_sphinx.php @@ -147,7 +147,7 @@ class phpbb_search_fulltext_sphinx } // Check if directory paths have been filled - if (!$this->config['fulltext_sphinx_data_path'] || !$this->config['fulltext_sphinx_config_path']) + if (!$this->config['fulltext_sphinx_data_path']) { $this->config_file_data = $this->user->lang('FULLTEXT_SPHINX_NO_CONFIG_DATA'); return false; @@ -225,7 +225,7 @@ class phpbb_search_fulltext_sphinx array('source', 'source_phpbb_' . $this->id . '_main'), array('docinfo', 'extern'), array('morphology', 'none'), - array('stopwords', $this->config['fulltext_sphinx_stopwords'] ? $this->config['fulltext_sphinx_config_path'] . 'sphinx_stopwords.txt' : ''), + array('stopwords', ''), array('min_word_len', '2'), array('charset_type', 'utf-8'), array('charset_table', 'U+FF10..U+FF19->0..9, 0..9, U+FF41..U+FF5A->a..z, U+FF21..U+FF3A->a..z, A..Z->a..z, a..z, U+0149, U+017F, U+0138, U+00DF, U+00FF, U+00C0..U+00D6->U+00E0..U+00F6, U+00E0..U+00F6, U+00D8..U+00DE->U+00F8..U+00FE, U+00F8..U+00FE, U+0100->U+0101, U+0101, U+0102->U+0103, U+0103, U+0104->U+0105, U+0105, U+0106->U+0107, U+0107, U+0108->U+0109, U+0109, U+010A->U+010B, U+010B, U+010C->U+010D, U+010D, U+010E->U+010F, U+010F, U+0110->U+0111, U+0111, U+0112->U+0113, U+0113, U+0114->U+0115, U+0115, U+0116->U+0117, U+0117, U+0118->U+0119, U+0119, U+011A->U+011B, U+011B, U+011C->U+011D, U+011D, U+011E->U+011F, U+011F, U+0130->U+0131, U+0131, U+0132->U+0133, U+0133, U+0134->U+0135, U+0135, U+0136->U+0137, U+0137, U+0139->U+013A, U+013A, U+013B->U+013C, U+013C, U+013D->U+013E, U+013E, U+013F->U+0140, U+0140, U+0141->U+0142, U+0142, U+0143->U+0144, U+0144, U+0145->U+0146, U+0146, U+0147->U+0148, U+0148, U+014A->U+014B, U+014B, U+014C->U+014D, U+014D, U+014E->U+014F, U+014F, U+0150->U+0151, U+0151, U+0152->U+0153, U+0153, U+0154->U+0155, U+0155, U+0156->U+0157, U+0157, U+0158->U+0159, U+0159, U+015A->U+015B, U+015B, U+015C->U+015D, U+015D, U+015E->U+015F, U+015F, U+0160->U+0161, U+0161, U+0162->U+0163, U+0163, U+0164->U+0165, U+0165, U+0166->U+0167, U+0167, U+0168->U+0169, U+0169, U+016A->U+016B, U+016B, U+016C->U+016D, U+016D, U+016E->U+016F, U+016F, U+0170->U+0171, U+0171, U+0172->U+0173, U+0173, U+0174->U+0175, U+0175, U+0176->U+0177, U+0177, U+0178->U+00FF, U+00FF, U+0179->U+017A, U+017A, U+017B->U+017C, U+017C, U+017D->U+017E, U+017E, U+0410..U+042F->U+0430..U+044F, U+0430..U+044F, U+4E00..U+9FFF'), @@ -757,28 +757,18 @@ class phpbb_search_fulltext_sphinx function acp() { $config_vars = array( - 'fulltext_sphinx_config_path' => 'string', 'fulltext_sphinx_data_path' => 'string', 'fulltext_sphinx_host' => 'string', 'fulltext_sphinx_port' => 'int', - 'fulltext_sphinx_stopwords' => 'bool', 'fulltext_sphinx_indexer_mem_limit' => 'int', ); $tpl = ' ' . $this->user->lang['FULLTEXT_SPHINX_CONFIGURE']. ' -
-

' . $this->user->lang['FULLTEXT_SPHINX_CONFIG_PATH_EXPLAIN'] . '
-
-

' . $this->user->lang['FULLTEXT_SPHINX_DATA_PATH_EXPLAIN'] . '
-
-

' . $this->user->lang['FULLTEXT_SPHINX_STOPWORDS_FILE_EXPLAIN'] . '
-
-

' . $this->user->lang['FULLTEXT_SPHINX_HOST_EXPLAIN'] . '
diff --git a/phpBB/language/en/acp/search.php b/phpBB/language/en/acp/search.php index 99fbbac07e..6d9201fb11 100644 --- a/phpBB/language/en/acp/search.php +++ b/phpBB/language/en/acp/search.php @@ -69,8 +69,6 @@ $lang = array_merge($lang, array( 'FULLTEXT_POSTGRES_MIN_WORD_LEN_EXPLAIN' => 'Words with at least this many characters will be included in the query to the database.', 'FULLTEXT_POSTGRES_MAX_WORD_LEN_EXPLAIN' => 'Words with no more than this many characters will be included in the query to the database.', - 'FULLTEXT_SPHINX_CONFIG_PATH' => 'Path to config directory', - 'FULLTEXT_SPHINX_CONFIG_PATH_EXPLAIN' => 'You should put the sphinx.conf file in this directory. This config directory should be outside the web accessible directories.', 'FULLTEXT_SPHINX_CONFIGURE' => 'Configure the following settings to generate sphinx config file', 'FULLTEXT_SPHINX_DATA_PATH' => 'Path to data directory', 'FULLTEXT_SPHINX_DATA_PATH_EXPLAIN' => 'It will be used to store the indexes and log files. You should create this directory outside the web accessable directories.', @@ -83,8 +81,6 @@ $lang = array_merge($lang, array( 'FULLTEXT_SPHINX_PORT' => 'Sphinx search daemon port', 'FULLTEXT_SPHINX_PORT_EXPLAIN' => 'Port on which the sphinx search daemon (searchd) listens. Leave empty to use the default 3312', 'FULLTEXT_SPHINX_WRONG_DATABASE' => 'The sphinx search for phpBB supports MySQL and PostgreSQL only.', - 'FULLTEXT_SPHINX_STOPWORDS_FILE' => 'Stopwords activated', - 'FULLTEXT_SPHINX_STOPWORDS_FILE_EXPLAIN' => 'You can place a file called sphinx_stopwords.txt containing one word in each line in your config directory. If this file is present these words will be excluded from the indexing process.', 'FULLTEXT_SPHINX_CONFIG_FILE' => 'Sphinx config file', 'FULLTEXT_SPHINX_CONFIG_FILE_EXPLAIN' => 'The generated content of the sphinx config file. This data needs to be pasted into the sphinx.conf which is used by sphinx search daemon.', 'FULLTEXT_SPHINX_NO_CONFIG_DATA' => 'The sphinx data and config directory paths are not defined. Please define them to generate the config file.', -- cgit v1.2.1 From 39bac86f7db881a1035bebad56507145103218d5 Mon Sep 17 00:00:00 2001 From: Dhruv Date: Sun, 22 Jul 2012 03:43:50 +0530 Subject: [feature/sphinx-fulltext-search] improve port option Use listen instead of deprecated port value in sphinx config file. sqlhost uses default $dbhost. PHPBB3-10946 --- phpBB/docs/sphinx.sample.conf | 5 ++--- phpBB/includes/search/fulltext_sphinx.php | 7 +++---- phpBB/language/en/acp/search.php | 2 +- 3 files changed, 6 insertions(+), 8 deletions(-) (limited to 'phpBB') diff --git a/phpBB/docs/sphinx.sample.conf b/phpBB/docs/sphinx.sample.conf index d0a897e0cc..aa0e8d905d 100644 --- a/phpBB/docs/sphinx.sample.conf +++ b/phpBB/docs/sphinx.sample.conf @@ -5,7 +5,7 @@ source source_phpbb_{SPHINX_ID}_main sql_user = username sql_pass = password sql_db = db_name - sql_port = 3306 #optional, default is 3306 + sql_port = 3306 #optional, default is 3306 for mysql and 5432 for pgsql sql_query_pre = SET NAMES 'utf8' sql_query_pre = UPDATE phpbb_sphinx SET max_doc_id = MAX(post_id) WHERE counter_id = 1 sql_query_range = SELECT MIN(post_id), MAX(post_id) FROM phpbb_posts @@ -85,8 +85,7 @@ indexer searchd { compat_sphinxql_magics = 0 - listen = 127.0.0.1 - port = 3312 + listen = localhost:9312 log = {DATA_PATH}/log/searchd.log query_log = {DATA_PATH}/log/sphinx-query.log read_timeout = 5 diff --git a/phpBB/includes/search/fulltext_sphinx.php b/phpBB/includes/search/fulltext_sphinx.php index e1052ee7da..18037a2be0 100644 --- a/phpBB/includes/search/fulltext_sphinx.php +++ b/phpBB/includes/search/fulltext_sphinx.php @@ -166,7 +166,7 @@ class phpbb_search_fulltext_sphinx array('sql_user', $dbuser), array('sql_pass', $dbpasswd), array('sql_db', $dbname), - array('sql_port', $this->config['fulltext_sphinx_port']), + array('sql_port', $dbport), array('sql_query_pre', 'SET NAMES \'utf8\''), array('sql_query_pre', 'UPDATE ' . SPHINX_TABLE . ' SET max_doc_id = (SELECT MAX(post_id) FROM ' . POSTS_TABLE . ') WHERE counter_id = 1'), array('sql_query_range', 'SELECT MIN(post_id), MAX(post_id) FROM ' . POSTS_TABLE . ''), @@ -241,8 +241,7 @@ class phpbb_search_fulltext_sphinx ), 'searchd' => array( array('compat_sphinxql_magics' , '0'), - array('listen' , '127.0.0.1'), - array('port', ($this->config['fulltext_sphinx_port']) ? $this->config['fulltext_sphinx_port'] : '3312'), + array('listen' , 'localhost' . ':' . ($this->config['fulltext_sphinx_port'] ? $this->config['fulltext_sphinx_port'] : '3312')), array('log', $this->config['fulltext_sphinx_data_path'] . 'log/searchd.log'), array('query_log', $this->config['fulltext_sphinx_data_path'] . 'log/sphinx-query.log'), array('read_timeout', '5'), @@ -759,7 +758,7 @@ class phpbb_search_fulltext_sphinx $config_vars = array( 'fulltext_sphinx_data_path' => 'string', 'fulltext_sphinx_host' => 'string', - 'fulltext_sphinx_port' => 'int', + 'fulltext_sphinx_port' => 'string', 'fulltext_sphinx_indexer_mem_limit' => 'int', ); diff --git a/phpBB/language/en/acp/search.php b/phpBB/language/en/acp/search.php index 6d9201fb11..394d408fdb 100644 --- a/phpBB/language/en/acp/search.php +++ b/phpBB/language/en/acp/search.php @@ -79,7 +79,7 @@ $lang = array_merge($lang, array( 'FULLTEXT_SPHINX_INDEXER_MEM_LIMIT_EXPLAIN' => 'This number should at all times be lower than the RAM available on your machine. If you experience periodic performance problems this might be due to the indexer consuming too many resources. It might help to lower the amount of memory available to the indexer.', 'FULLTEXT_SPHINX_MAIN_POSTS' => 'Number of posts in main index', 'FULLTEXT_SPHINX_PORT' => 'Sphinx search daemon port', - 'FULLTEXT_SPHINX_PORT_EXPLAIN' => 'Port on which the sphinx search daemon (searchd) listens. Leave empty to use the default 3312', + 'FULLTEXT_SPHINX_PORT_EXPLAIN' => 'Port on which the sphinx search daemon (searchd) listens. Leave empty to use the default Sphinx API port 3312 ', 'FULLTEXT_SPHINX_WRONG_DATABASE' => 'The sphinx search for phpBB supports MySQL and PostgreSQL only.', 'FULLTEXT_SPHINX_CONFIG_FILE' => 'Sphinx config file', 'FULLTEXT_SPHINX_CONFIG_FILE_EXPLAIN' => 'The generated content of the sphinx config file. This data needs to be pasted into the sphinx.conf which is used by sphinx search daemon.', -- cgit v1.2.1 From 9f2faaa8f1799cf794388604dbf7946488be2376 Mon Sep 17 00:00:00 2001 From: Dhruv Date: Sun, 22 Jul 2012 04:09:59 +0530 Subject: [feature/sphinx-fulltext-search] add trailing slash in language PHPBB3-10946 --- phpBB/language/en/acp/search.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'phpBB') diff --git a/phpBB/language/en/acp/search.php b/phpBB/language/en/acp/search.php index 394d408fdb..c52448743c 100644 --- a/phpBB/language/en/acp/search.php +++ b/phpBB/language/en/acp/search.php @@ -71,7 +71,7 @@ $lang = array_merge($lang, array( 'FULLTEXT_SPHINX_CONFIGURE' => 'Configure the following settings to generate sphinx config file', 'FULLTEXT_SPHINX_DATA_PATH' => 'Path to data directory', - 'FULLTEXT_SPHINX_DATA_PATH_EXPLAIN' => 'It will be used to store the indexes and log files. You should create this directory outside the web accessable directories.', + 'FULLTEXT_SPHINX_DATA_PATH_EXPLAIN' => 'It will be used to store the indexes and log files. You should create this directory outside the web accessible directories. (should have a trailing slash)', 'FULLTEXT_SPHINX_DELTA_POSTS' => 'Number of posts in frequently updated delta index', 'FULLTEXT_SPHINX_HOST' => 'SQL server host', 'FULLTEXT_SPHINX_HOST_EXPLAIN' => 'SQL server host, which the sphinx search daemon (searchd) connects to. Leave empty to use the default SQL server host', -- cgit v1.2.1 From 747af894a0b8d47079e704c66dcfce8ce00a7251 Mon Sep 17 00:00:00 2001 From: Dhruv Date: Thu, 26 Jul 2012 16:18:53 +0530 Subject: [feature/sphinx-fulltext-search] fixing comments Use // for two liners in comments. PHPBB3-10946 --- phpBB/includes/search/fulltext_sphinx.php | 4 ++-- phpBB/includes/search/sphinx/config.php | 32 +++++++++++++++---------------- 2 files changed, 18 insertions(+), 18 deletions(-) (limited to 'phpBB') diff --git a/phpBB/includes/search/fulltext_sphinx.php b/phpBB/includes/search/fulltext_sphinx.php index 18037a2be0..9319a57236 100644 --- a/phpBB/includes/search/fulltext_sphinx.php +++ b/phpBB/includes/search/fulltext_sphinx.php @@ -483,8 +483,8 @@ class phpbb_search_fulltext_sphinx $this->sphinx->SetLimits($start, (int) $per_page, SPHINX_MAX_MATCHES); $result = $this->sphinx->Query($search_query_prefix . str_replace('"', '"', $this->search_query), $this->indexes); - /* Could be connection to localhost:3312 failed (errno=111, - msg=Connection refused) during rotate, retry if so */ + // Could be connection to localhost:3312 failed (errno=111, + // msg=Connection refused) during rotate, retry if so $retries = SPHINX_CONNECT_RETRIES; while (!$result && (strpos($this->sphinx->_error, "errno=111,") !== false) && $retries--) { diff --git a/phpBB/includes/search/sphinx/config.php b/phpBB/includes/search/sphinx/config.php index 795dff07ed..f1864f0c8c 100644 --- a/phpBB/includes/search/sphinx/config.php +++ b/phpBB/includes/search/sphinx/config.php @@ -91,8 +91,8 @@ class phpbb_search_sphinx_config foreach ($config_data as $i => $line) { - /* If the value of a variable continues to the next line because the line - break was escaped then we don't trim leading space but treat it as a part of the value */ + // If the value of a variable continues to the next line because the line + // break was escaped then we don't trim leading space but treat it as a part of the value if ($in_value) { $line = rtrim($line); @@ -105,8 +105,8 @@ class phpbb_search_sphinx_config // If we're not inside a section look for one if (!$section) { - /* Add empty lines and comments as comment objects to the section list - that way they're not deleted when reassembling the file from the sections*/ + // Add empty lines and comments as comment objects to the section list + // that way they're not deleted when reassembling the file from the sections if (!$line || $line[0] == '#') { $this->sections[] = new phpbb_search_sphinx_config_comment($config_file[$i]); @@ -114,8 +114,8 @@ class phpbb_search_sphinx_config } else { - /* Otherwise we scan the line reading the section name until we find - an opening curly bracket or a comment */ + // Otherwise we scan the line reading the section name until we find + // an opening curly bracket or a comment $section_name = ''; $section_name_comment = ''; $found_opening_bracket = false; @@ -154,16 +154,16 @@ class phpbb_search_sphinx_config // If we're not in a value continuing over the line feed if (!$in_value) { - /* Then add empty lines and comments as comment objects to the variable list - of this section so they're not deleted on reassembly */ + // Then add empty lines and comments as comment objects to the variable list + // of this section so they're not deleted on reassembly if (!$line || $line[0] == '#') { $section->add_variable(new phpbb_search_sphinx_config_comment($config_file[$i])); continue; } - /* As long as we haven't yet actually found an opening bracket for this section - we treat everything as comments so it's not deleted either */ + // As long as we haven't yet actually found an opening bracket for this section + // we treat everything as comments so it's not deleted either if (!$found_opening_bracket) { if ($line[0] == '{') @@ -180,8 +180,8 @@ class phpbb_search_sphinx_config } } - /* If we did not find a comment in this line or still add to the previous - line's value ... */ + // If we did not find a comment in this line or still add to the previous + // line's value ... if ($line || $in_value) { if (!$in_value) @@ -239,8 +239,8 @@ class phpbb_search_sphinx_config } } - /* If a name and an equal sign were found then we have append a - new variable object to the section */ + // If a name and an equal sign were found then we have append a + // new variable object to the section if ($name && $found_assignment) { $section->add_variable(new phpbb_search_sphinx_config_variable(trim($name), trim($value), ($end_section) ? '' : $comment)); @@ -259,8 +259,8 @@ class phpbb_search_sphinx_config } } - /* If we did not find anything meaningful up to here, then just treat it - as a comment */ + // If we did not find anything meaningful up to here, then just treat it + // as a comment $comment = ($skip_first) ? "\t" . substr(ltrim($config_file[$i]), 1) : $config_file[$i]; $section->add_variable(new phpbb_search_sphinx_config_comment($comment)); } -- cgit v1.2.1 From eb4298c646d2b41a46dfef5ad2a72ea520b13539 Mon Sep 17 00:00:00 2001 From: Dhruv Date: Thu, 26 Jul 2012 16:55:11 +0530 Subject: [feature/sphinx-fulltext-search] coding changes acc to phbb conventions Add a new line after break. Change docblocks to be more informative. PHPBB3-10946 --- phpBB/includes/search/fulltext_sphinx.php | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) (limited to 'phpBB') diff --git a/phpBB/includes/search/fulltext_sphinx.php b/phpBB/includes/search/fulltext_sphinx.php index 9319a57236..303b6feae8 100644 --- a/phpBB/includes/search/fulltext_sphinx.php +++ b/phpBB/includes/search/fulltext_sphinx.php @@ -103,7 +103,7 @@ class phpbb_search_fulltext_sphinx /** * Checks permissions and paths, if everything is correct it generates the config file * - * @return string|bool Language key of the error/incompatiblity occured + * @return string|bool Language key of the error/incompatiblity encountered, or false if successful * * @access public */ @@ -381,14 +381,19 @@ class phpbb_search_fulltext_sphinx case 'a': $this->sphinx->SetGroupBy('topic_id', SPH_GROUPBY_ATTR, 'poster_id ' . (($sort_dir == 'a') ? 'ASC' : 'DESC')); break; + case 'f': $this->sphinx->SetGroupBy('topic_id', SPH_GROUPBY_ATTR, 'forum_id ' . (($sort_dir == 'a') ? 'ASC' : 'DESC')); break; + case 'i': + case 's': $this->sphinx->SetGroupBy('topic_id', SPH_GROUPBY_ATTR, 'post_subject ' . (($sort_dir == 'a') ? 'ASC' : 'DESC')); break; + case 't': + default: $this->sphinx->SetGroupBy('topic_id', SPH_GROUPBY_ATTR, 'topic_last_post_time ' . (($sort_dir == 'a') ? 'ASC' : 'DESC')); break; @@ -401,14 +406,19 @@ class phpbb_search_fulltext_sphinx case 'a': $this->sphinx->SetSortMode(($sort_dir == 'a') ? SPH_SORT_ATTR_ASC : SPH_SORT_ATTR_DESC, 'poster_id'); break; + case 'f': $this->sphinx->SetSortMode(($sort_dir == 'a') ? SPH_SORT_ATTR_ASC : SPH_SORT_ATTR_DESC, 'forum_id'); break; + case 'i': + case 's': $this->sphinx->SetSortMode(($sort_dir == 'a') ? SPH_SORT_ATTR_ASC : SPH_SORT_ATTR_DESC, 'post_subject'); break; + case 't': + default: $this->sphinx->SetSortMode(($sort_dir == 'a') ? SPH_SORT_ATTR_ASC : SPH_SORT_ATTR_DESC, 'post_time'); break; @@ -619,7 +629,7 @@ class phpbb_search_fulltext_sphinx } /** - * Destroy old cache entries + * Nothing needs to be destroyed * * @access public */ -- cgit v1.2.1 From a6b5b2784fc45764bce077481c40572b49c8b60d Mon Sep 17 00:00:00 2001 From: Dhruv Date: Thu, 26 Jul 2012 17:32:06 +0530 Subject: [feature/sphinx-fulltext-search] fix sphinx for arbitary host PHPBB3-10946 --- phpBB/includes/search/fulltext_sphinx.php | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) (limited to 'phpBB') diff --git a/phpBB/includes/search/fulltext_sphinx.php b/phpBB/includes/search/fulltext_sphinx.php index 303b6feae8..3d16438ab8 100644 --- a/phpBB/includes/search/fulltext_sphinx.php +++ b/phpBB/includes/search/fulltext_sphinx.php @@ -82,8 +82,7 @@ class phpbb_search_fulltext_sphinx $this->sphinx = new SphinxClient(); - // We only support localhost for now - $this->sphinx->SetServer('localhost', (isset($this->config['fulltext_sphinx_port']) && $this->config['fulltext_sphinx_port']) ? (int) $this->config['fulltext_sphinx_port'] : 3312); + $this->sphinx->SetServer(($this->config['fulltext_sphinx_host'] ? $this->config['fulltext_sphinx_host'] : 'localhost'), ($this->config['fulltext_sphinx_port'] ? (int) $this->config['fulltext_sphinx_port'] : 3312)); $error = false; } @@ -162,7 +161,8 @@ class phpbb_search_fulltext_sphinx $config_data = array( 'source source_phpbb_' . $this->id . '_main' => array( array('type', $this->dbtype), - array('sql_host', $this->config['fulltext_sphinx_host'] ? $this->config['fulltext_sphinx_host'] : $dbhost), + // This config value sql_host needs to be changed incase sphinx and sql are on different servers + array('sql_host', $dbhost), array('sql_user', $dbuser), array('sql_pass', $dbpasswd), array('sql_db', $dbname), @@ -241,7 +241,7 @@ class phpbb_search_fulltext_sphinx ), 'searchd' => array( array('compat_sphinxql_magics' , '0'), - array('listen' , 'localhost' . ':' . ($this->config['fulltext_sphinx_port'] ? $this->config['fulltext_sphinx_port'] : '3312')), + array('listen' , ($this->config['fulltext_sphinx_host'] ? $this->config['fulltext_sphinx_host'] : 'localhost') . ':' . ($this->config['fulltext_sphinx_port'] ? $this->config['fulltext_sphinx_port'] : '3312')), array('log', $this->config['fulltext_sphinx_data_path'] . 'log/searchd.log'), array('query_log', $this->config['fulltext_sphinx_data_path'] . 'log/sphinx-query.log'), array('read_timeout', '5'), -- cgit v1.2.1 From 3f4afedad34e8bc9a25d0636dee0cf182dd8a5eb Mon Sep 17 00:00:00 2001 From: Dhruv Date: Fri, 27 Jul 2012 02:40:05 +0530 Subject: [feature/sphinx-fulltext-search] fix language of host config PHPBB3-10946 --- phpBB/language/en/acp/search.php | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) (limited to 'phpBB') diff --git a/phpBB/language/en/acp/search.php b/phpBB/language/en/acp/search.php index c52448743c..038e77c190 100644 --- a/phpBB/language/en/acp/search.php +++ b/phpBB/language/en/acp/search.php @@ -73,13 +73,13 @@ $lang = array_merge($lang, array( 'FULLTEXT_SPHINX_DATA_PATH' => 'Path to data directory', 'FULLTEXT_SPHINX_DATA_PATH_EXPLAIN' => 'It will be used to store the indexes and log files. You should create this directory outside the web accessible directories. (should have a trailing slash)', 'FULLTEXT_SPHINX_DELTA_POSTS' => 'Number of posts in frequently updated delta index', - 'FULLTEXT_SPHINX_HOST' => 'SQL server host', - 'FULLTEXT_SPHINX_HOST_EXPLAIN' => 'SQL server host, which the sphinx search daemon (searchd) connects to. Leave empty to use the default SQL server host', + 'FULLTEXT_SPHINX_HOST' => 'Sphinx search daemon host', + 'FULLTEXT_SPHINX_HOST_EXPLAIN' => 'Host on which the sphinx search daemon (searchd) listens. Leave empty to use the default localhost', 'FULLTEXT_SPHINX_INDEXER_MEM_LIMIT' => 'Indexer memory limit', 'FULLTEXT_SPHINX_INDEXER_MEM_LIMIT_EXPLAIN' => 'This number should at all times be lower than the RAM available on your machine. If you experience periodic performance problems this might be due to the indexer consuming too many resources. It might help to lower the amount of memory available to the indexer.', 'FULLTEXT_SPHINX_MAIN_POSTS' => 'Number of posts in main index', 'FULLTEXT_SPHINX_PORT' => 'Sphinx search daemon port', - 'FULLTEXT_SPHINX_PORT_EXPLAIN' => 'Port on which the sphinx search daemon (searchd) listens. Leave empty to use the default Sphinx API port 3312 ', + 'FULLTEXT_SPHINX_PORT_EXPLAIN' => 'Port on which the sphinx search daemon (searchd) listens. Leave empty to use the default Sphinx API port 3312', 'FULLTEXT_SPHINX_WRONG_DATABASE' => 'The sphinx search for phpBB supports MySQL and PostgreSQL only.', 'FULLTEXT_SPHINX_CONFIG_FILE' => 'Sphinx config file', 'FULLTEXT_SPHINX_CONFIG_FILE_EXPLAIN' => 'The generated content of the sphinx config file. This data needs to be pasted into the sphinx.conf which is used by sphinx search daemon.', -- cgit v1.2.1 From 654798922574ef086d618b50a8a8d16eceaa5320 Mon Sep 17 00:00:00 2001 From: Dhruv Date: Fri, 27 Jul 2012 02:48:25 +0530 Subject: [feature/sphinx-fulltext-search] use 9312 as default port Uses 9312 instead of 3312 as default port for searchd to listen on according to latest sphinx documentation. Use filename sphinxapi.php instead of old one. PHPBB3-10946 --- phpBB/includes/search/fulltext_sphinx.php | 8 ++++---- phpBB/language/en/acp/search.php | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) (limited to 'phpBB') diff --git a/phpBB/includes/search/fulltext_sphinx.php b/phpBB/includes/search/fulltext_sphinx.php index 3d16438ab8..e9772239bf 100644 --- a/phpBB/includes/search/fulltext_sphinx.php +++ b/phpBB/includes/search/fulltext_sphinx.php @@ -22,7 +22,7 @@ if (!defined('IN_PHPBB')) * function and the variables used are in global space. */ global $phpbb_root_path, $phpEx, $table_prefix; -require($phpbb_root_path . 'includes/sphinxapi-0.9.8.' . $phpEx); +require($phpbb_root_path . 'includes/sphinxapi.' . $phpEx); define('SPHINX_MAX_MATCHES', 20000); define('SPHINX_CONNECT_RETRIES', 3); @@ -82,7 +82,7 @@ class phpbb_search_fulltext_sphinx $this->sphinx = new SphinxClient(); - $this->sphinx->SetServer(($this->config['fulltext_sphinx_host'] ? $this->config['fulltext_sphinx_host'] : 'localhost'), ($this->config['fulltext_sphinx_port'] ? (int) $this->config['fulltext_sphinx_port'] : 3312)); + $this->sphinx->SetServer(($this->config['fulltext_sphinx_host'] ? $this->config['fulltext_sphinx_host'] : 'localhost'), ($this->config['fulltext_sphinx_port'] ? (int) $this->config['fulltext_sphinx_port'] : 9312)); $error = false; } @@ -241,7 +241,7 @@ class phpbb_search_fulltext_sphinx ), 'searchd' => array( array('compat_sphinxql_magics' , '0'), - array('listen' , ($this->config['fulltext_sphinx_host'] ? $this->config['fulltext_sphinx_host'] : 'localhost') . ':' . ($this->config['fulltext_sphinx_port'] ? $this->config['fulltext_sphinx_port'] : '3312')), + array('listen' , ($this->config['fulltext_sphinx_host'] ? $this->config['fulltext_sphinx_host'] : 'localhost') . ':' . ($this->config['fulltext_sphinx_port'] ? $this->config['fulltext_sphinx_port'] : '9312')), array('log', $this->config['fulltext_sphinx_data_path'] . 'log/searchd.log'), array('query_log', $this->config['fulltext_sphinx_data_path'] . 'log/sphinx-query.log'), array('read_timeout', '5'), @@ -493,7 +493,7 @@ class phpbb_search_fulltext_sphinx $this->sphinx->SetLimits($start, (int) $per_page, SPHINX_MAX_MATCHES); $result = $this->sphinx->Query($search_query_prefix . str_replace('"', '"', $this->search_query), $this->indexes); - // Could be connection to localhost:3312 failed (errno=111, + // Could be connection to localhost:9312 failed (errno=111, // msg=Connection refused) during rotate, retry if so $retries = SPHINX_CONNECT_RETRIES; while (!$result && (strpos($this->sphinx->_error, "errno=111,") !== false) && $retries--) diff --git a/phpBB/language/en/acp/search.php b/phpBB/language/en/acp/search.php index 038e77c190..9f947dc816 100644 --- a/phpBB/language/en/acp/search.php +++ b/phpBB/language/en/acp/search.php @@ -79,7 +79,7 @@ $lang = array_merge($lang, array( 'FULLTEXT_SPHINX_INDEXER_MEM_LIMIT_EXPLAIN' => 'This number should at all times be lower than the RAM available on your machine. If you experience periodic performance problems this might be due to the indexer consuming too many resources. It might help to lower the amount of memory available to the indexer.', 'FULLTEXT_SPHINX_MAIN_POSTS' => 'Number of posts in main index', 'FULLTEXT_SPHINX_PORT' => 'Sphinx search daemon port', - 'FULLTEXT_SPHINX_PORT_EXPLAIN' => 'Port on which the sphinx search daemon (searchd) listens. Leave empty to use the default Sphinx API port 3312', + 'FULLTEXT_SPHINX_PORT_EXPLAIN' => 'Port on which the sphinx search daemon (searchd) listens. Leave empty to use the default Sphinx API port 9312', 'FULLTEXT_SPHINX_WRONG_DATABASE' => 'The sphinx search for phpBB supports MySQL and PostgreSQL only.', 'FULLTEXT_SPHINX_CONFIG_FILE' => 'Sphinx config file', 'FULLTEXT_SPHINX_CONFIG_FILE_EXPLAIN' => 'The generated content of the sphinx config file. This data needs to be pasted into the sphinx.conf which is used by sphinx search daemon.', -- cgit v1.2.1 From cec9f7d54e360bc2b6b4b12c3123d9c5216d2b62 Mon Sep 17 00:00:00 2001 From: Dhruv Date: Fri, 27 Jul 2012 02:49:44 +0530 Subject: [feature/sphinx-fulltext-search] remove unused property Removes unused property $word_length PHPBB3-10946 --- phpBB/includes/search/fulltext_sphinx.php | 1 - 1 file changed, 1 deletion(-) (limited to 'phpBB') diff --git a/phpBB/includes/search/fulltext_sphinx.php b/phpBB/includes/search/fulltext_sphinx.php index e9772239bf..9cf6f41fa0 100644 --- a/phpBB/includes/search/fulltext_sphinx.php +++ b/phpBB/includes/search/fulltext_sphinx.php @@ -47,7 +47,6 @@ class phpbb_search_fulltext_sphinx private $dbtype; private $user; private $config_file_data = ''; - public $word_length = array(); public $search_query; public $common_words = array(); -- cgit v1.2.1 From fe8a0d3bc6767f43e7df0b6c964a7f19fa3a5ccc Mon Sep 17 00:00:00 2001 From: Dhruv Date: Fri, 27 Jul 2012 10:50:20 +0530 Subject: [feature/sphinx-fulltext-search] fix auth bug $this->auth replaces $auth as at other occurences of auth. PHPBB3-10946 --- phpBB/includes/search/fulltext_sphinx.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'phpBB') diff --git a/phpBB/includes/search/fulltext_sphinx.php b/phpBB/includes/search/fulltext_sphinx.php index 9cf6f41fa0..8371f6b377 100644 --- a/phpBB/includes/search/fulltext_sphinx.php +++ b/phpBB/includes/search/fulltext_sphinx.php @@ -477,7 +477,7 @@ class phpbb_search_fulltext_sphinx if (sizeof($ex_fid_ary)) { // All forums that a user is allowed to access - $fid_ary = array_unique(array_intersect(array_keys($this->auth->acl_getf('f_read', true)), array_keys($auth->acl_getf('f_search', true)))); + $fid_ary = array_unique(array_intersect(array_keys($this->auth->acl_getf('f_read', true)), array_keys($this->auth->acl_getf('f_search', true)))); // All forums that the user wants to and can search in $search_forums = array_diff($fid_ary, $ex_fid_ary); -- cgit v1.2.1 From 033a2328c4f4451d6ec1f3eb310b5ad4e846e28e Mon Sep 17 00:00:00 2001 From: Dhruv Date: Fri, 27 Jul 2012 11:27:25 +0530 Subject: [feature/sphinx-fulltext-search] add sphinxapi.php file PHPBB3-10946 --- phpBB/includes/sphinxapi.php | 1712 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 1712 insertions(+) create mode 100644 phpBB/includes/sphinxapi.php (limited to 'phpBB') diff --git a/phpBB/includes/sphinxapi.php b/phpBB/includes/sphinxapi.php new file mode 100644 index 0000000000..bd83b1d2e0 --- /dev/null +++ b/phpBB/includes/sphinxapi.php @@ -0,0 +1,1712 @@ +=8 ) + { + $v = (int)$v; + return pack ( "NN", $v>>32, $v&0xFFFFFFFF ); + } + + // x32, int + if ( is_int($v) ) + return pack ( "NN", $v < 0 ? -1 : 0, $v ); + + // x32, bcmath + if ( function_exists("bcmul") ) + { + if ( bccomp ( $v, 0 ) == -1 ) + $v = bcadd ( "18446744073709551616", $v ); + $h = bcdiv ( $v, "4294967296", 0 ); + $l = bcmod ( $v, "4294967296" ); + return pack ( "NN", (float)$h, (float)$l ); // conversion to float is intentional; int would lose 31st bit + } + + // x32, no-bcmath + $p = max(0, strlen($v) - 13); + $lo = abs((float)substr($v, $p)); + $hi = abs((float)substr($v, 0, $p)); + + $m = $lo + $hi*1316134912.0; // (10 ^ 13) % (1 << 32) = 1316134912 + $q = floor($m/4294967296.0); + $l = $m - ($q*4294967296.0); + $h = $hi*2328.0 + $q; // (10 ^ 13) / (1 << 32) = 2328 + + if ( $v<0 ) + { + if ( $l==0 ) + $h = 4294967296.0 - $h; + else + { + $h = 4294967295.0 - $h; + $l = 4294967296.0 - $l; + } + } + return pack ( "NN", $h, $l ); +} + +/// pack 64-bit unsigned +function sphPackU64 ( $v ) +{ + assert ( is_numeric($v) ); + + // x64 + if ( PHP_INT_SIZE>=8 ) + { + assert ( $v>=0 ); + + // x64, int + if ( is_int($v) ) + return pack ( "NN", $v>>32, $v&0xFFFFFFFF ); + + // x64, bcmath + if ( function_exists("bcmul") ) + { + $h = bcdiv ( $v, 4294967296, 0 ); + $l = bcmod ( $v, 4294967296 ); + return pack ( "NN", $h, $l ); + } + + // x64, no-bcmath + $p = max ( 0, strlen($v) - 13 ); + $lo = (int)substr ( $v, $p ); + $hi = (int)substr ( $v, 0, $p ); + + $m = $lo + $hi*1316134912; + $l = $m % 4294967296; + $h = $hi*2328 + (int)($m/4294967296); + + return pack ( "NN", $h, $l ); + } + + // x32, int + if ( is_int($v) ) + return pack ( "NN", 0, $v ); + + // x32, bcmath + if ( function_exists("bcmul") ) + { + $h = bcdiv ( $v, "4294967296", 0 ); + $l = bcmod ( $v, "4294967296" ); + return pack ( "NN", (float)$h, (float)$l ); // conversion to float is intentional; int would lose 31st bit + } + + // x32, no-bcmath + $p = max(0, strlen($v) - 13); + $lo = (float)substr($v, $p); + $hi = (float)substr($v, 0, $p); + + $m = $lo + $hi*1316134912.0; + $q = floor($m / 4294967296.0); + $l = $m - ($q * 4294967296.0); + $h = $hi*2328.0 + $q; + + return pack ( "NN", $h, $l ); +} + +// unpack 64-bit unsigned +function sphUnpackU64 ( $v ) +{ + list ( $hi, $lo ) = array_values ( unpack ( "N*N*", $v ) ); + + if ( PHP_INT_SIZE>=8 ) + { + if ( $hi<0 ) $hi += (1<<32); // because php 5.2.2 to 5.2.5 is totally fucked up again + if ( $lo<0 ) $lo += (1<<32); + + // x64, int + if ( $hi<=2147483647 ) + return ($hi<<32) + $lo; + + // x64, bcmath + if ( function_exists("bcmul") ) + return bcadd ( $lo, bcmul ( $hi, "4294967296" ) ); + + // x64, no-bcmath + $C = 100000; + $h = ((int)($hi / $C) << 32) + (int)($lo / $C); + $l = (($hi % $C) << 32) + ($lo % $C); + if ( $l>$C ) + { + $h += (int)($l / $C); + $l = $l % $C; + } + + if ( $h==0 ) + return $l; + return sprintf ( "%d%05d", $h, $l ); + } + + // x32, int + if ( $hi==0 ) + { + if ( $lo>0 ) + return $lo; + return sprintf ( "%u", $lo ); + } + + $hi = sprintf ( "%u", $hi ); + $lo = sprintf ( "%u", $lo ); + + // x32, bcmath + if ( function_exists("bcmul") ) + return bcadd ( $lo, bcmul ( $hi, "4294967296" ) ); + + // x32, no-bcmath + $hi = (float)$hi; + $lo = (float)$lo; + + $q = floor($hi/10000000.0); + $r = $hi - $q*10000000.0; + $m = $lo + $r*4967296.0; + $mq = floor($m/10000000.0); + $l = $m - $mq*10000000.0; + $h = $q*4294967296.0 + $r*429.0 + $mq; + + $h = sprintf ( "%.0f", $h ); + $l = sprintf ( "%07.0f", $l ); + if ( $h=="0" ) + return sprintf( "%.0f", (float)$l ); + return $h . $l; +} + +// unpack 64-bit signed +function sphUnpackI64 ( $v ) +{ + list ( $hi, $lo ) = array_values ( unpack ( "N*N*", $v ) ); + + // x64 + if ( PHP_INT_SIZE>=8 ) + { + if ( $hi<0 ) $hi += (1<<32); // because php 5.2.2 to 5.2.5 is totally fucked up again + if ( $lo<0 ) $lo += (1<<32); + + return ($hi<<32) + $lo; + } + + // x32, int + if ( $hi==0 ) + { + if ( $lo>0 ) + return $lo; + return sprintf ( "%u", $lo ); + } + // x32, int + elseif ( $hi==-1 ) + { + if ( $lo<0 ) + return $lo; + return sprintf ( "%.0f", $lo - 4294967296.0 ); + } + + $neg = ""; + $c = 0; + if ( $hi<0 ) + { + $hi = ~$hi; + $lo = ~$lo; + $c = 1; + $neg = "-"; + } + + $hi = sprintf ( "%u", $hi ); + $lo = sprintf ( "%u", $lo ); + + // x32, bcmath + if ( function_exists("bcmul") ) + return $neg . bcadd ( bcadd ( $lo, bcmul ( $hi, "4294967296" ) ), $c ); + + // x32, no-bcmath + $hi = (float)$hi; + $lo = (float)$lo; + + $q = floor($hi/10000000.0); + $r = $hi - $q*10000000.0; + $m = $lo + $r*4967296.0; + $mq = floor($m/10000000.0); + $l = $m - $mq*10000000.0 + $c; + $h = $q*4294967296.0 + $r*429.0 + $mq; + if ( $l==10000000 ) + { + $l = 0; + $h += 1; + } + + $h = sprintf ( "%.0f", $h ); + $l = sprintf ( "%07.0f", $l ); + if ( $h=="0" ) + return $neg . sprintf( "%.0f", (float)$l ); + return $neg . $h . $l; +} + + +function sphFixUint ( $value ) +{ + if ( PHP_INT_SIZE>=8 ) + { + // x64 route, workaround broken unpack() in 5.2.2+ + if ( $value<0 ) $value += (1<<32); + return $value; + } + else + { + // x32 route, workaround php signed/unsigned braindamage + return sprintf ( "%u", $value ); + } +} + + +/// sphinx searchd client class +class SphinxClient +{ + var $_host; ///< searchd host (default is "localhost") + var $_port; ///< searchd port (default is 9312) + var $_offset; ///< how many records to seek from result-set start (default is 0) + var $_limit; ///< how many records to return from result-set starting at offset (default is 20) + var $_mode; ///< query matching mode (default is SPH_MATCH_ALL) + var $_weights; ///< per-field weights (default is 1 for all fields) + var $_sort; ///< match sorting mode (default is SPH_SORT_RELEVANCE) + var $_sortby; ///< attribute to sort by (defualt is "") + var $_min_id; ///< min ID to match (default is 0, which means no limit) + var $_max_id; ///< max ID to match (default is 0, which means no limit) + var $_filters; ///< search filters + var $_groupby; ///< group-by attribute name + var $_groupfunc; ///< group-by function (to pre-process group-by attribute value with) + var $_groupsort; ///< group-by sorting clause (to sort groups in result set with) + var $_groupdistinct;///< group-by count-distinct attribute + var $_maxmatches; ///< max matches to retrieve + var $_cutoff; ///< cutoff to stop searching at (default is 0) + var $_retrycount; ///< distributed retries count + var $_retrydelay; ///< distributed retries delay + var $_anchor; ///< geographical anchor point + var $_indexweights; ///< per-index weights + var $_ranker; ///< ranking mode (default is SPH_RANK_PROXIMITY_BM25) + var $_rankexpr; ///< ranking mode expression (for SPH_RANK_EXPR) + var $_maxquerytime; ///< max query time, milliseconds (default is 0, do not limit) + var $_fieldweights; ///< per-field-name weights + var $_overrides; ///< per-query attribute values overrides + var $_select; ///< select-list (attributes or expressions, with optional aliases) + + var $_error; ///< last error message + var $_warning; ///< last warning message + var $_connerror; ///< connection error vs remote error flag + + var $_reqs; ///< requests array for multi-query + var $_mbenc; ///< stored mbstring encoding + var $_arrayresult; ///< whether $result["matches"] should be a hash or an array + var $_timeout; ///< connect timeout + + ///////////////////////////////////////////////////////////////////////////// + // common stuff + ///////////////////////////////////////////////////////////////////////////// + + /// create a new client object and fill defaults + function SphinxClient () + { + // per-client-object settings + $this->_host = "localhost"; + $this->_port = 9312; + $this->_path = false; + $this->_socket = false; + + // per-query settings + $this->_offset = 0; + $this->_limit = 20; + $this->_mode = SPH_MATCH_ALL; + $this->_weights = array (); + $this->_sort = SPH_SORT_RELEVANCE; + $this->_sortby = ""; + $this->_min_id = 0; + $this->_max_id = 0; + $this->_filters = array (); + $this->_groupby = ""; + $this->_groupfunc = SPH_GROUPBY_DAY; + $this->_groupsort = "@group desc"; + $this->_groupdistinct= ""; + $this->_maxmatches = 1000; + $this->_cutoff = 0; + $this->_retrycount = 0; + $this->_retrydelay = 0; + $this->_anchor = array (); + $this->_indexweights= array (); + $this->_ranker = SPH_RANK_PROXIMITY_BM25; + $this->_rankexpr = ""; + $this->_maxquerytime= 0; + $this->_fieldweights= array(); + $this->_overrides = array(); + $this->_select = "*"; + + $this->_error = ""; // per-reply fields (for single-query case) + $this->_warning = ""; + $this->_connerror = false; + + $this->_reqs = array (); // requests storage (for multi-query case) + $this->_mbenc = ""; + $this->_arrayresult = false; + $this->_timeout = 0; + } + + function __destruct() + { + if ( $this->_socket !== false ) + fclose ( $this->_socket ); + } + + /// get last error message (string) + function GetLastError () + { + return $this->_error; + } + + /// get last warning message (string) + function GetLastWarning () + { + return $this->_warning; + } + + /// get last error flag (to tell network connection errors from searchd errors or broken responses) + function IsConnectError() + { + return $this->_connerror; + } + + /// set searchd host name (string) and port (integer) + function SetServer ( $host, $port = 0 ) + { + assert ( is_string($host) ); + if ( $host[0] == '/') + { + $this->_path = 'unix://' . $host; + return; + } + if ( substr ( $host, 0, 7 )=="unix://" ) + { + $this->_path = $host; + return; + } + + assert ( is_int($port) ); + $this->_host = $host; + $this->_port = $port; + $this->_path = ''; + + } + + /// set server connection timeout (0 to remove) + function SetConnectTimeout ( $timeout ) + { + assert ( is_numeric($timeout) ); + $this->_timeout = $timeout; + } + + + function _Send ( $handle, $data, $length ) + { + if ( feof($handle) || fwrite ( $handle, $data, $length ) !== $length ) + { + $this->_error = 'connection unexpectedly closed (timed out?)'; + $this->_connerror = true; + return false; + } + return true; + } + + ///////////////////////////////////////////////////////////////////////////// + + /// enter mbstring workaround mode + function _MBPush () + { + $this->_mbenc = ""; + if ( ini_get ( "mbstring.func_overload" ) & 2 ) + { + $this->_mbenc = mb_internal_encoding(); + mb_internal_encoding ( "latin1" ); + } + } + + /// leave mbstring workaround mode + function _MBPop () + { + if ( $this->_mbenc ) + mb_internal_encoding ( $this->_mbenc ); + } + + /// connect to searchd server + function _Connect () + { + if ( $this->_socket!==false ) + { + // we are in persistent connection mode, so we have a socket + // however, need to check whether it's still alive + if ( !@feof ( $this->_socket ) ) + return $this->_socket; + + // force reopen + $this->_socket = false; + } + + $errno = 0; + $errstr = ""; + $this->_connerror = false; + + if ( $this->_path ) + { + $host = $this->_path; + $port = 0; + } + else + { + $host = $this->_host; + $port = $this->_port; + } + + if ( $this->_timeout<=0 ) + $fp = @fsockopen ( $host, $port, $errno, $errstr ); + else + $fp = @fsockopen ( $host, $port, $errno, $errstr, $this->_timeout ); + + if ( !$fp ) + { + if ( $this->_path ) + $location = $this->_path; + else + $location = "{$this->_host}:{$this->_port}"; + + $errstr = trim ( $errstr ); + $this->_error = "connection to $location failed (errno=$errno, msg=$errstr)"; + $this->_connerror = true; + return false; + } + + // send my version + // this is a subtle part. we must do it before (!) reading back from searchd. + // because otherwise under some conditions (reported on FreeBSD for instance) + // TCP stack could throttle write-write-read pattern because of Nagle. + if ( !$this->_Send ( $fp, pack ( "N", 1 ), 4 ) ) + { + fclose ( $fp ); + $this->_error = "failed to send client protocol version"; + return false; + } + + // check version + list(,$v) = unpack ( "N*", fread ( $fp, 4 ) ); + $v = (int)$v; + if ( $v<1 ) + { + fclose ( $fp ); + $this->_error = "expected searchd protocol version 1+, got version '$v'"; + return false; + } + + return $fp; + } + + /// get and check response packet from searchd server + function _GetResponse ( $fp, $client_ver ) + { + $response = ""; + $len = 0; + + $header = fread ( $fp, 8 ); + if ( strlen($header)==8 ) + { + list ( $status, $ver, $len ) = array_values ( unpack ( "n2a/Nb", $header ) ); + $left = $len; + while ( $left>0 && !feof($fp) ) + { + $chunk = fread ( $fp, min ( 8192, $left ) ); + if ( $chunk ) + { + $response .= $chunk; + $left -= strlen($chunk); + } + } + } + if ( $this->_socket === false ) + fclose ( $fp ); + + // check response + $read = strlen ( $response ); + if ( !$response || $read!=$len ) + { + $this->_error = $len + ? "failed to read searchd response (status=$status, ver=$ver, len=$len, read=$read)" + : "received zero-sized searchd response"; + return false; + } + + // check status + if ( $status==SEARCHD_WARNING ) + { + list(,$wlen) = unpack ( "N*", substr ( $response, 0, 4 ) ); + $this->_warning = substr ( $response, 4, $wlen ); + return substr ( $response, 4+$wlen ); + } + if ( $status==SEARCHD_ERROR ) + { + $this->_error = "searchd error: " . substr ( $response, 4 ); + return false; + } + if ( $status==SEARCHD_RETRY ) + { + $this->_error = "temporary searchd error: " . substr ( $response, 4 ); + return false; + } + if ( $status!=SEARCHD_OK ) + { + $this->_error = "unknown status code '$status'"; + return false; + } + + // check version + if ( $ver<$client_ver ) + { + $this->_warning = sprintf ( "searchd command v.%d.%d older than client's v.%d.%d, some options might not work", + $ver>>8, $ver&0xff, $client_ver>>8, $client_ver&0xff ); + } + + return $response; + } + + ///////////////////////////////////////////////////////////////////////////// + // searching + ///////////////////////////////////////////////////////////////////////////// + + /// set offset and count into result set, + /// and optionally set max-matches and cutoff limits + function SetLimits ( $offset, $limit, $max=0, $cutoff=0 ) + { + assert ( is_int($offset) ); + assert ( is_int($limit) ); + assert ( $offset>=0 ); + assert ( $limit>0 ); + assert ( $max>=0 ); + $this->_offset = $offset; + $this->_limit = $limit; + if ( $max>0 ) + $this->_maxmatches = $max; + if ( $cutoff>0 ) + $this->_cutoff = $cutoff; + } + + /// set maximum query time, in milliseconds, per-index + /// integer, 0 means "do not limit" + function SetMaxQueryTime ( $max ) + { + assert ( is_int($max) ); + assert ( $max>=0 ); + $this->_maxquerytime = $max; + } + + /// set matching mode + function SetMatchMode ( $mode ) + { + assert ( $mode==SPH_MATCH_ALL + || $mode==SPH_MATCH_ANY + || $mode==SPH_MATCH_PHRASE + || $mode==SPH_MATCH_BOOLEAN + || $mode==SPH_MATCH_EXTENDED + || $mode==SPH_MATCH_FULLSCAN + || $mode==SPH_MATCH_EXTENDED2 ); + $this->_mode = $mode; + } + + /// set ranking mode + function SetRankingMode ( $ranker, $rankexpr="" ) + { + assert ( $ranker>=0 && $ranker_ranker = $ranker; + $this->_rankexpr = $rankexpr; + } + + /// set matches sorting mode + function SetSortMode ( $mode, $sortby="" ) + { + assert ( + $mode==SPH_SORT_RELEVANCE || + $mode==SPH_SORT_ATTR_DESC || + $mode==SPH_SORT_ATTR_ASC || + $mode==SPH_SORT_TIME_SEGMENTS || + $mode==SPH_SORT_EXTENDED || + $mode==SPH_SORT_EXPR ); + assert ( is_string($sortby) ); + assert ( $mode==SPH_SORT_RELEVANCE || strlen($sortby)>0 ); + + $this->_sort = $mode; + $this->_sortby = $sortby; + } + + /// bind per-field weights by order + /// DEPRECATED; use SetFieldWeights() instead + function SetWeights ( $weights ) + { + assert ( is_array($weights) ); + foreach ( $weights as $weight ) + assert ( is_int($weight) ); + + $this->_weights = $weights; + } + + /// bind per-field weights by name + function SetFieldWeights ( $weights ) + { + assert ( is_array($weights) ); + foreach ( $weights as $name=>$weight ) + { + assert ( is_string($name) ); + assert ( is_int($weight) ); + } + $this->_fieldweights = $weights; + } + + /// bind per-index weights by name + function SetIndexWeights ( $weights ) + { + assert ( is_array($weights) ); + foreach ( $weights as $index=>$weight ) + { + assert ( is_string($index) ); + assert ( is_int($weight) ); + } + $this->_indexweights = $weights; + } + + /// set IDs range to match + /// only match records if document ID is beetwen $min and $max (inclusive) + function SetIDRange ( $min, $max ) + { + assert ( is_numeric($min) ); + assert ( is_numeric($max) ); + assert ( $min<=$max ); + $this->_min_id = $min; + $this->_max_id = $max; + } + + /// set values set filter + /// only match records where $attribute value is in given set + function SetFilter ( $attribute, $values, $exclude=false ) + { + assert ( is_string($attribute) ); + assert ( is_array($values) ); + assert ( count($values) ); + + if ( is_array($values) && count($values) ) + { + foreach ( $values as $value ) + assert ( is_numeric($value) ); + + $this->_filters[] = array ( "type"=>SPH_FILTER_VALUES, "attr"=>$attribute, "exclude"=>$exclude, "values"=>$values ); + } + } + + /// set range filter + /// only match records if $attribute value is beetwen $min and $max (inclusive) + function SetFilterRange ( $attribute, $min, $max, $exclude=false ) + { + assert ( is_string($attribute) ); + assert ( is_numeric($min) ); + assert ( is_numeric($max) ); + assert ( $min<=$max ); + + $this->_filters[] = array ( "type"=>SPH_FILTER_RANGE, "attr"=>$attribute, "exclude"=>$exclude, "min"=>$min, "max"=>$max ); + } + + /// set float range filter + /// only match records if $attribute value is beetwen $min and $max (inclusive) + function SetFilterFloatRange ( $attribute, $min, $max, $exclude=false ) + { + assert ( is_string($attribute) ); + assert ( is_float($min) ); + assert ( is_float($max) ); + assert ( $min<=$max ); + + $this->_filters[] = array ( "type"=>SPH_FILTER_FLOATRANGE, "attr"=>$attribute, "exclude"=>$exclude, "min"=>$min, "max"=>$max ); + } + + /// setup anchor point for geosphere distance calculations + /// required to use @geodist in filters and sorting + /// latitude and longitude must be in radians + function SetGeoAnchor ( $attrlat, $attrlong, $lat, $long ) + { + assert ( is_string($attrlat) ); + assert ( is_string($attrlong) ); + assert ( is_float($lat) ); + assert ( is_float($long) ); + + $this->_anchor = array ( "attrlat"=>$attrlat, "attrlong"=>$attrlong, "lat"=>$lat, "long"=>$long ); + } + + /// set grouping attribute and function + function SetGroupBy ( $attribute, $func, $groupsort="@group desc" ) + { + assert ( is_string($attribute) ); + assert ( is_string($groupsort) ); + assert ( $func==SPH_GROUPBY_DAY + || $func==SPH_GROUPBY_WEEK + || $func==SPH_GROUPBY_MONTH + || $func==SPH_GROUPBY_YEAR + || $func==SPH_GROUPBY_ATTR + || $func==SPH_GROUPBY_ATTRPAIR ); + + $this->_groupby = $attribute; + $this->_groupfunc = $func; + $this->_groupsort = $groupsort; + } + + /// set count-distinct attribute for group-by queries + function SetGroupDistinct ( $attribute ) + { + assert ( is_string($attribute) ); + $this->_groupdistinct = $attribute; + } + + /// set distributed retries count and delay + function SetRetries ( $count, $delay=0 ) + { + assert ( is_int($count) && $count>=0 ); + assert ( is_int($delay) && $delay>=0 ); + $this->_retrycount = $count; + $this->_retrydelay = $delay; + } + + /// set result set format (hash or array; hash by default) + /// PHP specific; needed for group-by-MVA result sets that may contain duplicate IDs + function SetArrayResult ( $arrayresult ) + { + assert ( is_bool($arrayresult) ); + $this->_arrayresult = $arrayresult; + } + + /// set attribute values override + /// there can be only one override per attribute + /// $values must be a hash that maps document IDs to attribute values + function SetOverride ( $attrname, $attrtype, $values ) + { + assert ( is_string ( $attrname ) ); + assert ( in_array ( $attrtype, array ( SPH_ATTR_INTEGER, SPH_ATTR_TIMESTAMP, SPH_ATTR_BOOL, SPH_ATTR_FLOAT, SPH_ATTR_BIGINT ) ) ); + assert ( is_array ( $values ) ); + + $this->_overrides[$attrname] = array ( "attr"=>$attrname, "type"=>$attrtype, "values"=>$values ); + } + + /// set select-list (attributes or expressions), SQL-like syntax + function SetSelect ( $select ) + { + assert ( is_string ( $select ) ); + $this->_select = $select; + } + + ////////////////////////////////////////////////////////////////////////////// + + /// clear all filters (for multi-queries) + function ResetFilters () + { + $this->_filters = array(); + $this->_anchor = array(); + } + + /// clear groupby settings (for multi-queries) + function ResetGroupBy () + { + $this->_groupby = ""; + $this->_groupfunc = SPH_GROUPBY_DAY; + $this->_groupsort = "@group desc"; + $this->_groupdistinct= ""; + } + + /// clear all attribute value overrides (for multi-queries) + function ResetOverrides () + { + $this->_overrides = array (); + } + + ////////////////////////////////////////////////////////////////////////////// + + /// connect to searchd server, run given search query through given indexes, + /// and return the search results + function Query ( $query, $index="*", $comment="" ) + { + assert ( empty($this->_reqs) ); + + $this->AddQuery ( $query, $index, $comment ); + $results = $this->RunQueries (); + $this->_reqs = array (); // just in case it failed too early + + if ( !is_array($results) ) + return false; // probably network error; error message should be already filled + + $this->_error = $results[0]["error"]; + $this->_warning = $results[0]["warning"]; + if ( $results[0]["status"]==SEARCHD_ERROR ) + return false; + else + return $results[0]; + } + + /// helper to pack floats in network byte order + function _PackFloat ( $f ) + { + $t1 = pack ( "f", $f ); // machine order + list(,$t2) = unpack ( "L*", $t1 ); // int in machine order + return pack ( "N", $t2 ); + } + + /// add query to multi-query batch + /// returns index into results array from RunQueries() call + function AddQuery ( $query, $index="*", $comment="" ) + { + // mbstring workaround + $this->_MBPush (); + + // build request + $req = pack ( "NNNN", $this->_offset, $this->_limit, $this->_mode, $this->_ranker ); + if ( $this->_ranker==SPH_RANK_EXPR ) + $req .= pack ( "N", strlen($this->_rankexpr) ) . $this->_rankexpr; + $req .= pack ( "N", $this->_sort ); // (deprecated) sort mode + $req .= pack ( "N", strlen($this->_sortby) ) . $this->_sortby; + $req .= pack ( "N", strlen($query) ) . $query; // query itself + $req .= pack ( "N", count($this->_weights) ); // weights + foreach ( $this->_weights as $weight ) + $req .= pack ( "N", (int)$weight ); + $req .= pack ( "N", strlen($index) ) . $index; // indexes + $req .= pack ( "N", 1 ); // id64 range marker + $req .= sphPackU64 ( $this->_min_id ) . sphPackU64 ( $this->_max_id ); // id64 range + + // filters + $req .= pack ( "N", count($this->_filters) ); + foreach ( $this->_filters as $filter ) + { + $req .= pack ( "N", strlen($filter["attr"]) ) . $filter["attr"]; + $req .= pack ( "N", $filter["type"] ); + switch ( $filter["type"] ) + { + case SPH_FILTER_VALUES: + $req .= pack ( "N", count($filter["values"]) ); + foreach ( $filter["values"] as $value ) + $req .= sphPackI64 ( $value ); + break; + + case SPH_FILTER_RANGE: + $req .= sphPackI64 ( $filter["min"] ) . sphPackI64 ( $filter["max"] ); + break; + + case SPH_FILTER_FLOATRANGE: + $req .= $this->_PackFloat ( $filter["min"] ) . $this->_PackFloat ( $filter["max"] ); + break; + + default: + assert ( 0 && "internal error: unhandled filter type" ); + } + $req .= pack ( "N", $filter["exclude"] ); + } + + // group-by clause, max-matches count, group-sort clause, cutoff count + $req .= pack ( "NN", $this->_groupfunc, strlen($this->_groupby) ) . $this->_groupby; + $req .= pack ( "N", $this->_maxmatches ); + $req .= pack ( "N", strlen($this->_groupsort) ) . $this->_groupsort; + $req .= pack ( "NNN", $this->_cutoff, $this->_retrycount, $this->_retrydelay ); + $req .= pack ( "N", strlen($this->_groupdistinct) ) . $this->_groupdistinct; + + // anchor point + if ( empty($this->_anchor) ) + { + $req .= pack ( "N", 0 ); + } else + { + $a =& $this->_anchor; + $req .= pack ( "N", 1 ); + $req .= pack ( "N", strlen($a["attrlat"]) ) . $a["attrlat"]; + $req .= pack ( "N", strlen($a["attrlong"]) ) . $a["attrlong"]; + $req .= $this->_PackFloat ( $a["lat"] ) . $this->_PackFloat ( $a["long"] ); + } + + // per-index weights + $req .= pack ( "N", count($this->_indexweights) ); + foreach ( $this->_indexweights as $idx=>$weight ) + $req .= pack ( "N", strlen($idx) ) . $idx . pack ( "N", $weight ); + + // max query time + $req .= pack ( "N", $this->_maxquerytime ); + + // per-field weights + $req .= pack ( "N", count($this->_fieldweights) ); + foreach ( $this->_fieldweights as $field=>$weight ) + $req .= pack ( "N", strlen($field) ) . $field . pack ( "N", $weight ); + + // comment + $req .= pack ( "N", strlen($comment) ) . $comment; + + // attribute overrides + $req .= pack ( "N", count($this->_overrides) ); + foreach ( $this->_overrides as $key => $entry ) + { + $req .= pack ( "N", strlen($entry["attr"]) ) . $entry["attr"]; + $req .= pack ( "NN", $entry["type"], count($entry["values"]) ); + foreach ( $entry["values"] as $id=>$val ) + { + assert ( is_numeric($id) ); + assert ( is_numeric($val) ); + + $req .= sphPackU64 ( $id ); + switch ( $entry["type"] ) + { + case SPH_ATTR_FLOAT: $req .= $this->_PackFloat ( $val ); break; + case SPH_ATTR_BIGINT: $req .= sphPackI64 ( $val ); break; + default: $req .= pack ( "N", $val ); break; + } + } + } + + // select-list + $req .= pack ( "N", strlen($this->_select) ) . $this->_select; + + // mbstring workaround + $this->_MBPop (); + + // store request to requests array + $this->_reqs[] = $req; + return count($this->_reqs)-1; + } + + /// connect to searchd, run queries batch, and return an array of result sets + function RunQueries () + { + if ( empty($this->_reqs) ) + { + $this->_error = "no queries defined, issue AddQuery() first"; + return false; + } + + // mbstring workaround + $this->_MBPush (); + + if (!( $fp = $this->_Connect() )) + { + $this->_MBPop (); + return false; + } + + // send query, get response + $nreqs = count($this->_reqs); + $req = join ( "", $this->_reqs ); + $len = 8+strlen($req); + $req = pack ( "nnNNN", SEARCHD_COMMAND_SEARCH, VER_COMMAND_SEARCH, $len, 0, $nreqs ) . $req; // add header + + if ( !( $this->_Send ( $fp, $req, $len+8 ) ) || + !( $response = $this->_GetResponse ( $fp, VER_COMMAND_SEARCH ) ) ) + { + $this->_MBPop (); + return false; + } + + // query sent ok; we can reset reqs now + $this->_reqs = array (); + + // parse and return response + return $this->_ParseSearchResponse ( $response, $nreqs ); + } + + /// parse and return search query (or queries) response + function _ParseSearchResponse ( $response, $nreqs ) + { + $p = 0; // current position + $max = strlen($response); // max position for checks, to protect against broken responses + + $results = array (); + for ( $ires=0; $ires<$nreqs && $p<$max; $ires++ ) + { + $results[] = array(); + $result =& $results[$ires]; + + $result["error"] = ""; + $result["warning"] = ""; + + // extract status + list(,$status) = unpack ( "N*", substr ( $response, $p, 4 ) ); $p += 4; + $result["status"] = $status; + if ( $status!=SEARCHD_OK ) + { + list(,$len) = unpack ( "N*", substr ( $response, $p, 4 ) ); $p += 4; + $message = substr ( $response, $p, $len ); $p += $len; + + if ( $status==SEARCHD_WARNING ) + { + $result["warning"] = $message; + } else + { + $result["error"] = $message; + continue; + } + } + + // read schema + $fields = array (); + $attrs = array (); + + list(,$nfields) = unpack ( "N*", substr ( $response, $p, 4 ) ); $p += 4; + while ( $nfields-->0 && $p<$max ) + { + list(,$len) = unpack ( "N*", substr ( $response, $p, 4 ) ); $p += 4; + $fields[] = substr ( $response, $p, $len ); $p += $len; + } + $result["fields"] = $fields; + + list(,$nattrs) = unpack ( "N*", substr ( $response, $p, 4 ) ); $p += 4; + while ( $nattrs-->0 && $p<$max ) + { + list(,$len) = unpack ( "N*", substr ( $response, $p, 4 ) ); $p += 4; + $attr = substr ( $response, $p, $len ); $p += $len; + list(,$type) = unpack ( "N*", substr ( $response, $p, 4 ) ); $p += 4; + $attrs[$attr] = $type; + } + $result["attrs"] = $attrs; + + // read match count + list(,$count) = unpack ( "N*", substr ( $response, $p, 4 ) ); $p += 4; + list(,$id64) = unpack ( "N*", substr ( $response, $p, 4 ) ); $p += 4; + + // read matches + $idx = -1; + while ( $count-->0 && $p<$max ) + { + // index into result array + $idx++; + + // parse document id and weight + if ( $id64 ) + { + $doc = sphUnpackU64 ( substr ( $response, $p, 8 ) ); $p += 8; + list(,$weight) = unpack ( "N*", substr ( $response, $p, 4 ) ); $p += 4; + } + else + { + list ( $doc, $weight ) = array_values ( unpack ( "N*N*", + substr ( $response, $p, 8 ) ) ); + $p += 8; + $doc = sphFixUint($doc); + } + $weight = sprintf ( "%u", $weight ); + + // create match entry + if ( $this->_arrayresult ) + $result["matches"][$idx] = array ( "id"=>$doc, "weight"=>$weight ); + else + $result["matches"][$doc]["weight"] = $weight; + + // parse and create attributes + $attrvals = array (); + foreach ( $attrs as $attr=>$type ) + { + // handle 64bit ints + if ( $type==SPH_ATTR_BIGINT ) + { + $attrvals[$attr] = sphUnpackI64 ( substr ( $response, $p, 8 ) ); $p += 8; + continue; + } + + // handle floats + if ( $type==SPH_ATTR_FLOAT ) + { + list(,$uval) = unpack ( "N*", substr ( $response, $p, 4 ) ); $p += 4; + list(,$fval) = unpack ( "f*", pack ( "L", $uval ) ); + $attrvals[$attr] = $fval; + continue; + } + + // handle everything else as unsigned ints + list(,$val) = unpack ( "N*", substr ( $response, $p, 4 ) ); $p += 4; + if ( $type==SPH_ATTR_MULTI ) + { + $attrvals[$attr] = array (); + $nvalues = $val; + while ( $nvalues-->0 && $p<$max ) + { + list(,$val) = unpack ( "N*", substr ( $response, $p, 4 ) ); $p += 4; + $attrvals[$attr][] = sphFixUint($val); + } + } else if ( $type==SPH_ATTR_MULTI64 ) + { + $attrvals[$attr] = array (); + $nvalues = $val; + while ( $nvalues>0 && $p<$max ) + { + $attrvals[$attr][] = sphUnpackU64 ( substr ( $response, $p, 8 ) ); $p += 8; + $nvalues -= 2; + } + } else if ( $type==SPH_ATTR_STRING ) + { + $attrvals[$attr] = substr ( $response, $p, $val ); + $p += $val; + } else + { + $attrvals[$attr] = sphFixUint($val); + } + } + + if ( $this->_arrayresult ) + $result["matches"][$idx]["attrs"] = $attrvals; + else + $result["matches"][$doc]["attrs"] = $attrvals; + } + + list ( $total, $total_found, $msecs, $words ) = + array_values ( unpack ( "N*N*N*N*", substr ( $response, $p, 16 ) ) ); + $result["total"] = sprintf ( "%u", $total ); + $result["total_found"] = sprintf ( "%u", $total_found ); + $result["time"] = sprintf ( "%.3f", $msecs/1000 ); + $p += 16; + + while ( $words-->0 && $p<$max ) + { + list(,$len) = unpack ( "N*", substr ( $response, $p, 4 ) ); $p += 4; + $word = substr ( $response, $p, $len ); $p += $len; + list ( $docs, $hits ) = array_values ( unpack ( "N*N*", substr ( $response, $p, 8 ) ) ); $p += 8; + $result["words"][$word] = array ( + "docs"=>sprintf ( "%u", $docs ), + "hits"=>sprintf ( "%u", $hits ) ); + } + } + + $this->_MBPop (); + return $results; + } + + ///////////////////////////////////////////////////////////////////////////// + // excerpts generation + ///////////////////////////////////////////////////////////////////////////// + + /// connect to searchd server, and generate exceprts (snippets) + /// of given documents for given query. returns false on failure, + /// an array of snippets on success + function BuildExcerpts ( $docs, $index, $words, $opts=array() ) + { + assert ( is_array($docs) ); + assert ( is_string($index) ); + assert ( is_string($words) ); + assert ( is_array($opts) ); + + $this->_MBPush (); + + if (!( $fp = $this->_Connect() )) + { + $this->_MBPop(); + return false; + } + + ///////////////// + // fixup options + ///////////////// + + if ( !isset($opts["before_match"]) ) $opts["before_match"] = ""; + if ( !isset($opts["after_match"]) ) $opts["after_match"] = ""; + if ( !isset($opts["chunk_separator"]) ) $opts["chunk_separator"] = " ... "; + if ( !isset($opts["limit"]) ) $opts["limit"] = 256; + if ( !isset($opts["limit_passages"]) ) $opts["limit_passages"] = 0; + if ( !isset($opts["limit_words"]) ) $opts["limit_words"] = 0; + if ( !isset($opts["around"]) ) $opts["around"] = 5; + if ( !isset($opts["exact_phrase"]) ) $opts["exact_phrase"] = false; + if ( !isset($opts["single_passage"]) ) $opts["single_passage"] = false; + if ( !isset($opts["use_boundaries"]) ) $opts["use_boundaries"] = false; + if ( !isset($opts["weight_order"]) ) $opts["weight_order"] = false; + if ( !isset($opts["query_mode"]) ) $opts["query_mode"] = false; + if ( !isset($opts["force_all_words"]) ) $opts["force_all_words"] = false; + if ( !isset($opts["start_passage_id"]) ) $opts["start_passage_id"] = 1; + if ( !isset($opts["load_files"]) ) $opts["load_files"] = false; + if ( !isset($opts["html_strip_mode"]) ) $opts["html_strip_mode"] = "index"; + if ( !isset($opts["allow_empty"]) ) $opts["allow_empty"] = false; + if ( !isset($opts["passage_boundary"]) ) $opts["passage_boundary"] = "none"; + if ( !isset($opts["emit_zones"]) ) $opts["emit_zones"] = false; + if ( !isset($opts["load_files_scattered"]) ) $opts["load_files_scattered"] = false; + + + ///////////////// + // build request + ///////////////// + + // v.1.2 req + $flags = 1; // remove spaces + if ( $opts["exact_phrase"] ) $flags |= 2; + if ( $opts["single_passage"] ) $flags |= 4; + if ( $opts["use_boundaries"] ) $flags |= 8; + if ( $opts["weight_order"] ) $flags |= 16; + if ( $opts["query_mode"] ) $flags |= 32; + if ( $opts["force_all_words"] ) $flags |= 64; + if ( $opts["load_files"] ) $flags |= 128; + if ( $opts["allow_empty"] ) $flags |= 256; + if ( $opts["emit_zones"] ) $flags |= 512; + if ( $opts["load_files_scattered"] ) $flags |= 1024; + $req = pack ( "NN", 0, $flags ); // mode=0, flags=$flags + $req .= pack ( "N", strlen($index) ) . $index; // req index + $req .= pack ( "N", strlen($words) ) . $words; // req words + + // options + $req .= pack ( "N", strlen($opts["before_match"]) ) . $opts["before_match"]; + $req .= pack ( "N", strlen($opts["after_match"]) ) . $opts["after_match"]; + $req .= pack ( "N", strlen($opts["chunk_separator"]) ) . $opts["chunk_separator"]; + $req .= pack ( "NN", (int)$opts["limit"], (int)$opts["around"] ); + $req .= pack ( "NNN", (int)$opts["limit_passages"], (int)$opts["limit_words"], (int)$opts["start_passage_id"] ); // v.1.2 + $req .= pack ( "N", strlen($opts["html_strip_mode"]) ) . $opts["html_strip_mode"]; + $req .= pack ( "N", strlen($opts["passage_boundary"]) ) . $opts["passage_boundary"]; + + // documents + $req .= pack ( "N", count($docs) ); + foreach ( $docs as $doc ) + { + assert ( is_string($doc) ); + $req .= pack ( "N", strlen($doc) ) . $doc; + } + + //////////////////////////// + // send query, get response + //////////////////////////// + + $len = strlen($req); + $req = pack ( "nnN", SEARCHD_COMMAND_EXCERPT, VER_COMMAND_EXCERPT, $len ) . $req; // add header + if ( !( $this->_Send ( $fp, $req, $len+8 ) ) || + !( $response = $this->_GetResponse ( $fp, VER_COMMAND_EXCERPT ) ) ) + { + $this->_MBPop (); + return false; + } + + ////////////////// + // parse response + ////////////////// + + $pos = 0; + $res = array (); + $rlen = strlen($response); + for ( $i=0; $i $rlen ) + { + $this->_error = "incomplete reply"; + $this->_MBPop (); + return false; + } + $res[] = $len ? substr ( $response, $pos, $len ) : ""; + $pos += $len; + } + + $this->_MBPop (); + return $res; + } + + + ///////////////////////////////////////////////////////////////////////////// + // keyword generation + ///////////////////////////////////////////////////////////////////////////// + + /// connect to searchd server, and generate keyword list for a given query + /// returns false on failure, + /// an array of words on success + function BuildKeywords ( $query, $index, $hits ) + { + assert ( is_string($query) ); + assert ( is_string($index) ); + assert ( is_bool($hits) ); + + $this->_MBPush (); + + if (!( $fp = $this->_Connect() )) + { + $this->_MBPop(); + return false; + } + + ///////////////// + // build request + ///////////////// + + // v.1.0 req + $req = pack ( "N", strlen($query) ) . $query; // req query + $req .= pack ( "N", strlen($index) ) . $index; // req index + $req .= pack ( "N", (int)$hits ); + + //////////////////////////// + // send query, get response + //////////////////////////// + + $len = strlen($req); + $req = pack ( "nnN", SEARCHD_COMMAND_KEYWORDS, VER_COMMAND_KEYWORDS, $len ) . $req; // add header + if ( !( $this->_Send ( $fp, $req, $len+8 ) ) || + !( $response = $this->_GetResponse ( $fp, VER_COMMAND_KEYWORDS ) ) ) + { + $this->_MBPop (); + return false; + } + + ////////////////// + // parse response + ////////////////// + + $pos = 0; + $res = array (); + $rlen = strlen($response); + list(,$nwords) = unpack ( "N*", substr ( $response, $pos, 4 ) ); + $pos += 4; + for ( $i=0; $i<$nwords; $i++ ) + { + list(,$len) = unpack ( "N*", substr ( $response, $pos, 4 ) ); $pos += 4; + $tokenized = $len ? substr ( $response, $pos, $len ) : ""; + $pos += $len; + + list(,$len) = unpack ( "N*", substr ( $response, $pos, 4 ) ); $pos += 4; + $normalized = $len ? substr ( $response, $pos, $len ) : ""; + $pos += $len; + + $res[] = array ( "tokenized"=>$tokenized, "normalized"=>$normalized ); + + if ( $hits ) + { + list($ndocs,$nhits) = array_values ( unpack ( "N*N*", substr ( $response, $pos, 8 ) ) ); + $pos += 8; + $res [$i]["docs"] = $ndocs; + $res [$i]["hits"] = $nhits; + } + + if ( $pos > $rlen ) + { + $this->_error = "incomplete reply"; + $this->_MBPop (); + return false; + } + } + + $this->_MBPop (); + return $res; + } + + function EscapeString ( $string ) + { + $from = array ( '\\', '(',')','|','-','!','@','~','"','&', '/', '^', '$', '=' ); + $to = array ( '\\\\', '\(','\)','\|','\-','\!','\@','\~','\"', '\&', '\/', '\^', '\$', '\=' ); + + return str_replace ( $from, $to, $string ); + } + + ///////////////////////////////////////////////////////////////////////////// + // attribute updates + ///////////////////////////////////////////////////////////////////////////// + + /// batch update given attributes in given rows in given indexes + /// returns amount of updated documents (0 or more) on success, or -1 on failure + function UpdateAttributes ( $index, $attrs, $values, $mva=false ) + { + // verify everything + assert ( is_string($index) ); + assert ( is_bool($mva) ); + + assert ( is_array($attrs) ); + foreach ( $attrs as $attr ) + assert ( is_string($attr) ); + + assert ( is_array($values) ); + foreach ( $values as $id=>$entry ) + { + assert ( is_numeric($id) ); + assert ( is_array($entry) ); + assert ( count($entry)==count($attrs) ); + foreach ( $entry as $v ) + { + if ( $mva ) + { + assert ( is_array($v) ); + foreach ( $v as $vv ) + assert ( is_int($vv) ); + } else + assert ( is_int($v) ); + } + } + + // build request + $this->_MBPush (); + $req = pack ( "N", strlen($index) ) . $index; + + $req .= pack ( "N", count($attrs) ); + foreach ( $attrs as $attr ) + { + $req .= pack ( "N", strlen($attr) ) . $attr; + $req .= pack ( "N", $mva ? 1 : 0 ); + } + + $req .= pack ( "N", count($values) ); + foreach ( $values as $id=>$entry ) + { + $req .= sphPackU64 ( $id ); + foreach ( $entry as $v ) + { + $req .= pack ( "N", $mva ? count($v) : $v ); + if ( $mva ) + foreach ( $v as $vv ) + $req .= pack ( "N", $vv ); + } + } + + // connect, send query, get response + if (!( $fp = $this->_Connect() )) + { + $this->_MBPop (); + return -1; + } + + $len = strlen($req); + $req = pack ( "nnN", SEARCHD_COMMAND_UPDATE, VER_COMMAND_UPDATE, $len ) . $req; // add header + if ( !$this->_Send ( $fp, $req, $len+8 ) ) + { + $this->_MBPop (); + return -1; + } + + if (!( $response = $this->_GetResponse ( $fp, VER_COMMAND_UPDATE ) )) + { + $this->_MBPop (); + return -1; + } + + // parse response + list(,$updated) = unpack ( "N*", substr ( $response, 0, 4 ) ); + $this->_MBPop (); + return $updated; + } + + ///////////////////////////////////////////////////////////////////////////// + // persistent connections + ///////////////////////////////////////////////////////////////////////////// + + function Open() + { + if ( $this->_socket !== false ) + { + $this->_error = 'already connected'; + return false; + } + if ( !$fp = $this->_Connect() ) + return false; + + // command, command version = 0, body length = 4, body = 1 + $req = pack ( "nnNN", SEARCHD_COMMAND_PERSIST, 0, 4, 1 ); + if ( !$this->_Send ( $fp, $req, 12 ) ) + return false; + + $this->_socket = $fp; + return true; + } + + function Close() + { + if ( $this->_socket === false ) + { + $this->_error = 'not connected'; + return false; + } + + fclose ( $this->_socket ); + $this->_socket = false; + + return true; + } + + ////////////////////////////////////////////////////////////////////////// + // status + ////////////////////////////////////////////////////////////////////////// + + function Status () + { + $this->_MBPush (); + if (!( $fp = $this->_Connect() )) + { + $this->_MBPop(); + return false; + } + + $req = pack ( "nnNN", SEARCHD_COMMAND_STATUS, VER_COMMAND_STATUS, 4, 1 ); // len=4, body=1 + if ( !( $this->_Send ( $fp, $req, 12 ) ) || + !( $response = $this->_GetResponse ( $fp, VER_COMMAND_STATUS ) ) ) + { + $this->_MBPop (); + return false; + } + + $res = substr ( $response, 4 ); // just ignore length, error handling, etc + $p = 0; + list ( $rows, $cols ) = array_values ( unpack ( "N*N*", substr ( $response, $p, 8 ) ) ); $p += 8; + + $res = array(); + for ( $i=0; $i<$rows; $i++ ) + for ( $j=0; $j<$cols; $j++ ) + { + list(,$len) = unpack ( "N*", substr ( $response, $p, 4 ) ); $p += 4; + $res[$i][] = substr ( $response, $p, $len ); $p += $len; + } + + $this->_MBPop (); + return $res; + } + + ////////////////////////////////////////////////////////////////////////// + // flush + ////////////////////////////////////////////////////////////////////////// + + function FlushAttributes () + { + $this->_MBPush (); + if (!( $fp = $this->_Connect() )) + { + $this->_MBPop(); + return -1; + } + + $req = pack ( "nnN", SEARCHD_COMMAND_FLUSHATTRS, VER_COMMAND_FLUSHATTRS, 0 ); // len=0 + if ( !( $this->_Send ( $fp, $req, 8 ) ) || + !( $response = $this->_GetResponse ( $fp, VER_COMMAND_FLUSHATTRS ) ) ) + { + $this->_MBPop (); + return -1; + } + + $tag = -1; + if ( strlen($response)==4 ) + list(,$tag) = unpack ( "N*", $response ); + else + $this->_error = "unexpected response length"; + + $this->_MBPop (); + return $tag; + } +} + +// +// $Id: sphinxapi.php 3087 2012-01-30 23:07:35Z shodan $ +// -- cgit v1.2.1 From f1729281e6f69202730c0926f3799da516fd3ae9 Mon Sep 17 00:00:00 2001 From: Dhruv Date: Fri, 27 Jul 2012 11:28:14 +0530 Subject: [feature/sphinx-fulltext-search] add sphinx to Authors file PHPBB3-10946 --- phpBB/docs/AUTHORS | 1 + 1 file changed, 1 insertion(+) (limited to 'phpBB') diff --git a/phpBB/docs/AUTHORS b/phpBB/docs/AUTHORS index f0b4e25549..85653f04e2 100644 --- a/phpBB/docs/AUTHORS +++ b/phpBB/docs/AUTHORS @@ -75,6 +75,7 @@ Jabber Class (c) 2006 Flyspray.org, http://www.flyspray.org/ Chora (c) 2000-2006, The Horde Project. http://horde.org/chora/ Horde Project (c) 2000-2006, The Horde Project. http://horde.org/ jQuery (c) 2011, John Resig. http://jquery.com/ +Sphinx Technologies Inc (c) 2001-2012 Andrew Aksyonoff, http://sphinxsearch.com/ PHP License, version 3.0: Pear (c) 2001-2004 PHP Group, http://pear.php.net -- cgit v1.2.1 UٴNtd};yرov݇Ծd^!%K*. !>`9_0wtqA G'ӓ_HjܤRʦ0C?.S`1VKALSizXݡވ+L@J-$C IB~w^fmB 4h1_tG}R2Ԙz]ي7E}vTcVjcm`pUVfߠjqV|q!'&#sif)ij4oq|fTÈ-$R?c,8V؅ۑҞsc C!v"Ӳ[bw[Tu,- ɂNj_l"C XPO&^DLbWAM:ie(슓y% fK3Ь):-te.Tܠ73 &QW\%#xՏ; RrY^7I͗SiU3l G'].|7Gah@KhD-iLn$ ׭qI6(;|S ~WPeg _]k3EYgmȯd*d,%Ej3ALEH؎qFU&y*ʜ^0~z+lp[wԸۀ4Gc_qMHg'Xʛ'%8BvJP.#6l\P?@;TҭHND)jvKWzEh3=7$7>\no>9]~wYuʀ|g|'и0Zҏ ejZy}B o(K1.\C4iO1UEޒa!,w)0 +!)PqMgtIg>"\lhXNU A(t*dM|fX$F.vpy.;2Vj.>e:Ŵb ͵0g&MvPZWmkЍW-[ k˶}H/ :l]]Nd4E?$RZfTY#K>\P?,Jz||J@`ML _yw]Z'‡}t-;z8X70^sAAl'^9nGߍqF?׊MMI@Ôq7g!IǀRY]9?It3r p)eϦT@\sM9OVs$ W:?K#PN> Gۨu6_k{*iAg̹[ ]nk-ЕCu%oʖgrfFFbG_*5׊{׹PtK8ee̝hl|O7~ lDkNEHdn'dZ!}J=] 8(,K(ҭ=@}dfT6n:7RdR"ǃwǤPMt 7Th9 1/+~rml:xD5)s]ȒWS|7IC $ݕ,eVZ lʊ^kQ_6ˮ m-/@h_$溡Eu^i)0ڱ- TרTEUWҶ*1P(`pOӳ ֆc#Ӎ9k ԕL:ժ&U-s6&aۂ11i 蛝r ƹߑ 8VU ~υm7;rϓ ?>A$@N aUSby,Y^N>W\ՈI]cݙa)KooDTY!{L#l'܀yXN5\Ǫƙޔh+.&L7 m?o p{`Anݝo̶hs9L78߭Ľ8d& 4R|3+S;/Fҹ+PX]'S2GS|P;aȉƢwUXos[{],Vy/KaqmsҒsD=i`Ӄڹe2CU/.2`]B\',Ey6HH5+`F=}87U ( ˥ -hZ.Q W2al /f8a]J|c[H]m3p0P&Cpp$vՎ8 &ݸ+dIMغv ~hҲ}ܡ[ f.>ZB _BG׺[JӝwLDi?﬐uxjŶJ7AM>'luD#}AIԦDiE + KZQhr"I "O's5X?β =_6}|>A۠ZTgJ@gL"L܇+,2uqI!5 NqUσ=^sF&L=8&0sxFM% dXr:a DE*7kU 9q%Jmw_NK B"0 ٫m1IǬjD̹%p"q0"'i~`@lBiu s,},<˅]MZAR.(`e#ɔpj-軈_^d"B=UJ5b> jPJ$\>UHCSM{/-,)B(=/BU6}!/HR7i~tk$t׆pJ|1L Y`SI*VMiJPΉerDDF^cW blkfs}:Rnl+7+Ru,%N݉ȳRӍMhXr^ K/݀8@ ,t9dF&'Y`z+O|eo7o2\usCS-zw ET\5 IgQp-^r "i=)Csk鉝y| 29IYV0n-vq/٪Fm\V\"UK'rNƅE7aPx۵ xV^5iEi << (Xuj hٖ8X%To3~y8:f YGc/bС[.<ҥ؞.Ĕ˰@t\-  m"ɉ^^m\`Ψy'``G\qTp D ƹE8懦EV!=sۛNM+ᴵ>ՒO]rLq~϶\yR[._;1@ 4q2)e1[1AbشYi,OSaƩWr+)<[G 蝢h+]KؐmlDd O"|bUȋ= Zeugg&]^׭ʇSEnԴ1'aFG1 Iq;0}2L@gBF]lQE)uڰuOhx,=Oe^0kܩS*x.mfNS0 ~.@DyݤC(Ir]p\#;q,G.STtЏuת6w0vؖ /. o6`x>Wp} W(fL0D 5 WcΆ,w9SKU؇^;k\i0*_JFJG:S\lnj.\"T+9ęwz)G%CV.큯iTϕ P.2Q44H#ć]niNaԞ~N⛠w[pIAP9pm P?֊y Yl EDrezs?0&dX6ߖY> 4 No_񗁴{ /r~djG`VC{!AC,OfSɋ Ў kq7zm FyukahEfu5ڐ|Q*[U<-F2Vg6|"V a(\Đ컰 XI8 a }ywP,{1LxF;6I@{E1]+͓/Wb-~OfgG65-PXzNg+փDEQC0~\7\X?.8IamH9 ;%t%t4~I֙d B\+x7+><r6=UIVfO=HuC^X&ן#FZ4ccn$\ۜ_h@"z-s!]z- '-M /0ě3Ј57ik>mY@dB27bʪ&]Iw֚MQQrlg{`C@WtSA#.OQo(/3%+͗,(,Z;~(xb}f BR'yzUҴŔ-l m:gU+^D}` p+z}#*p x %h=U3vO8JS @EU.)VG hX.% R5][A^~A pUu1 HBF/Nw[e;`_lL8+%yGJ4ΩϣeCn5&I>`b0m UiX&n:?5"X"/sd0X K8{1r,E@叭bNG2&FV9+wR- ٞɺs)~ޫסՈA x]\#w*OVFГn$Ƒ Y[o%Fq6Υ*jP!蝙ެv@z*eлo]i3ڞ퓠|5 33p8fBr=kaqjtL0EeٳJHZM oMnm{ߒ4(3E'@f4x7,M6 .wh?gqSXk?1x _܂y$3v+f#]SI} K_.Yx2@-CR >vٶ+n> 7iame7l`Z0Z|C툆]*(g(\a#~ |qOFљ{s W@vTf8|n"Ƞp$Ok6!{y3?O<8J]5գȎQbEh9X1T`b sWSqpTomNN뽋؊D|=k{>|JU |.23,a6^{kH':F/&=M`nAp-_ rZP\4n\5=%gSsT\Q: ӏ.Hy[B`lq.%4Fś^JG WGl R&Y( בX'zŃ:ۗS/""JjEi=@AE^9s{aGdG'[|G)6&_ͬ ^i4VSQ{ hL(r1{pinI-.@G %£lP"tC67`7=6L|+;%s!z׋_(ؘS&hYU^Vp]X ֶ9Aj!3 gPElHywRydR:G{?U )ǧY%1KD?g LX\bA#+^6Upv XtxMCc@aTUtܣ+;ѻHge0瞏 $8ߑVI T(5:I : ^2a| sR~/,Z4{]a d!m5XΪ1*6xԺvb)Vv^q%S%3Ct6"_j:GW*+rxhe1ȩD*!p3z&k:},Nwct&#'.S*¹VŌGʀ0ڬ"^5`3qxN v3ObK@_U+:LuA/Hғm[@SS0u3 ʙRvxOҏ}Ρx&EΌ.Oόh2A/{[Z:WS$FvŁh,矧]}c?bv`{*l<ы텷tZmrkMya{Y,Oi>/UŲJr{oKgu_1*6 " -~I/Q7n!L "ظխx t޳. Q]nžf]k h4^;m@q1Y[8^gdv?qԆI~TCQ)98@4Yo O/MoWT/6WA(~.:܉˗v7M(Sv}bH@Tunor^/.1mzy5J#+TpN-.0ҷCJp34Hh_<*feTm0#zq3 prrjm<9YdJowϕ7zSD|9IcF10mGnoIrEeO#EtN9Qb: Jv TjY#|b@[{`zŮ u * I)p%Xݓ]g]u-?DE?,Ė V@Mi2q]gD%? 1z+h>1n@7,& Bb奼2LT>7Lkӄ_?$uchI;wQ"뮄cy|\`^~7S[__,-a^>5(ܴ$EBz{66)21H>5a@ 5'5;Sr,s;!gY g#Ǟ?T<2[;?JjѡO0Q=Y)"Ξ{Я:-Iarf 6F#`Zh}_8[lY)9͓{4 VYo kqY-O^sq7I/n{I\ػþ&1SZQ/S֮$wXe_%݃3Vd8Gq mM9IԂXc `у Pݼ + bz{+߃# Y^Kȴ1AgnV7CcY5{88-cΤ̢.c  1ۮjQoxIsZ}YxKnQ&cABhֿGnS&=Um}N' 1DKQo>f"^EPk\'gz>>Iam  qoԸ;؞j;W]]VI"jXշ~#3*3KT СuQfqM܉W`ICZe%3&[4-k,FZ5{ ӉZR7GBOk6ediyxf|/H ĻuME{(6 v BIՋ`TlxQH\Ug&, 5wT67H0PD!160j5WWJ0:lI[[NyxbI3.з~.!FK]6W\%π7"sd;h#.'ƪ(EFBEs/ξ:jur2ѧ5կiUA&0L ^FqOK eGa8h1E))nWd_zCCFާ ފyGH #%fթt 9"ڥL}yK\&V%&Tr4\%AcsP}r bu6y7pCmczb-.Q|Am=(j{a2M i;uJW7 _^73\\Ek*`eMzUWd͠L΍ÕiϦj[y'inG lFB`rMeUw^o %AzPEP6@l2ifƖ}~ȳ+xuȨ4>N~GY:t)_6ZxG0EC#$8#80 C`Skfhņ~?ox1iGKHCE\v,t9;sѠ:owB@/Ϗ< N,ˈDgmch@Cr۔T^2W46Z c&f(HQKKmه8YK<+~+5@da Ahug&i+2mIL'ΌXL P۬W~6PGfsJSٓ w"_'Ȑrjv4!w3 \4?`.At82ES >`|%9 1%o&kg}yu@4ֆ6({G&7$lւ g'򮔭*-J^{WZc&NqPpKjC\N˅aY N`J n (gs}06gNl۶„hjBd"p=)/Fӛn/+Avy5Q/"jks7WC#8,?!,NĞc/ cl34ϽKLPi94[PECyZicrM1k=YLܫڌH&cjpzfOC㬔V1SVҤ0xgV{;vA;qcCESa!fl8 8gt|SL;Xa:}~X-:f2ʡSΩUyIEG1:TlU u5 L6jg+7%+Ǻ7C:&ssT,@Rc.$*Y݈hJnb4 @OyaD#`!!QY>dqjԪ/^Z6!û#[Jm?MoT7}{4.ɠFra_na":Q.l/\S+ȀhyDu@v=UVP\Asn XjsȄ=s5<` =YcOvh28b-1DYC!؁ Huv8G  FZ8Ɔ= 8" gUm U%4rEw1Ph'L@߼dɰaD-q3BWfA 01bbB# d 6XLY^&˻ Tz8y[wk[6^Qҡ@z8,+EĽD`3C2*a I@ uL!id@سod0 >w@<4 Wt쩭IJlTuimf=Z$MVlѷ-l]%IKO)5.#ߖmRMauՏ]ռ}`{`4[jHu+M}( ;ReKlGr}Y%%B!9\+Rm0@e\摻G hH`a'qDY&mһQh{K[وǐ3j5  O!~SG[#@kulfc+;-E~" ԍg~ݕ~I ׹4-P0 qgU%qpoo"{,c'bꨛэ~K1рhCK/#6K;;%c}7g;8NjT;?W+Y8핀+&K-*ۿm< 6WlF4Y r~L6n8z.|}Zads6#CyM,⡯MUO Hmr$<\7-)K {x.ϒ܁㻽sfLJa߶|UtxE:"_%Xj0a;8b-T;fcoH !:N[=BگoJcanpB)q>;0 Q'3m6LɒQF ,.EX)ABG-ceaT5ô7ItV~Uc}}\2CR^_/!D1bFӸ)~DY!{<![ <\!~5zo|?D4 ¥kg3~[r5pk$y9v/Dy~kH։B!! ޥ3=0,*W}+[(??k$pBsgɞZ߄'+D%AEi@W*[/{20يG.h^.g/cP6GTYzW r5z>OY8zQ#\+]*Ux#JbMA_VW7w$ʩ5'ExH,l;g4!r}5dpf u4@|?1Bs wݡ+;v`NFӄw)2MF@l]ӛ;GJmƏ^.PFV{#08azv B1CY`IXqΒ i^ qu#*ɋzaI,e-d^ C0Wų%v$tDjjb%j~R S)Z@%@Ξ~%ip7HŠxP>#Yj +u܆bHozXVZAfT}"o ŽLxEݤlyDFud Q6鶂Sn5OT GMJ.iS$><CUy|I^ -dwfWLUawa,ÜPSBLH-x{M6xT2iٯMµPRx L,߈jGԐ[dFWZ+Ezh!QhW!ga`-Rv2&q<è@싀 8Щ?M|3ݱ*K<|Sϼ˼P1? {u?Q v|eD@Mm빀^l"'cBB4{ULv,}m 6 LӚ j_A oqXdh壥@`y:V{[3? >mLVϛrZ.%2+~53,3#e]橥gwgDG浨d l4ڳV)8Zƒ&`RR'P%ggHie!re&8OmLn6\BV6jt2~Mr@{C?9e44? Rv7wl^f$^Z#qd2Th/Dy\txΦَ輐BԃB>.렧ORyvm XG'Mn2fF'k¯)"uI.+޾Ж~ "fRB+aNs#Ceӣ'8ril-&ӄ`vSi}CtPMHO*!,K{uޑtzi% ׇk_-c) ]?Q](:dg _4 &o.a2:Yi\1K]ES"3sabi]A7AUO|ܠv[ R8]&pRgԵ;_A Z;c{}r/`Oe~- Ozm_$G2k?32[^yg!X@r1y hDꃍwqظU Erhrp% ,-RpTC˗KUflR:FV-6]BO{!q&?6L& *Y UQ|mr D VtQnj]C4DmeaAɮ_䝻L5t(\ԃٝ['}MZ|`;V[`:0['0Q{m`@ jwYװeo۞|%lqWiND /0=[C`bp9VcEo3@pUImmFO}`>0Ҡ5U];y~BpS ~d6/nzG`t?L|[bzq3pި z3С55K$1*%dG(ZT]C|&iG}@~zz&9R 1J-,X64Pb*Eyn6lvSZezxfhl#t1yj2cfL$Aw!~L'k'ڽyp¿>\"XHI([W_+|@_O$ik%QEθUV @AlDyS 7t"zGF*LD~ ͶNފWU)`ym[0L" q7 L/PBW JѲt֧=Rvb}dbI6cްT.r l$|qޘn 5젟Qc JAՀqDvIȪ3=.Ocv Zwo/-zj#8$C-g"n; +^uJCu䕓$À,[=h3Ru*f[OD*N!Xc~ zGE,$v͐(7D_ȤT4ƃyFF"9W6VTGD^ec nҷ`]Ė=,y|*wm05Kpo߉F*߸%kn̔(sIvee5C^ n] &iQ7Bz> ѓ 2^NY{[5'1aERe3m;y?9RPΣViC99tŶ7N(L\V0JMJ)34c4nD{u*BHÎ&m$$O  TGp!0g,}Ue=>^j==H`i0Jn9.!]PXb<[.ts6SEf{k -תƒn漷m&}h Df/sb9*%T )gG:Y'M'C}}v3! eC{bYxH"6pƿ(ZIj뜝K]t9LOiΆ)ZNTP:1՛`m1|K.;|Xf!|8vzȟ$58Z& x1s>Eۗ޼FA z;7W݁I##Un\!hS1})`'$7Eyc"2R#PJ_ESh!=!r7d˸A /dPTYeɵ LA^?z)oo-E&, i1{r3AK}9~,h61e4pg"Cgmֿ؃r@6 7nE:n (+fHZQO_X(TdM#ᮉ=h*(>ޓQ~USH\PK\4&* RAy|U^h_Q+cy0g!HvM~i3ej 6a~t ЍgJ|{Hdm5A}t|[h7czH!t{mօO}uT 3'ل9269% \SnԚv\JJbZ:,XK+\ZӈvydMVgҍfEMgEb D7.tz}<oG;I"^+ZIׄNZ(k9=9LZ9RfݼK٘Ks\0 C/9_녨n`0d~vH4;vRC&M$&(SQtJVUwXX*}@\?S ;!'ZjI9q,]+F; nӬ>ۍٮo^2;#V}eW_ؽ֗>]X`#ʕs Zv5aWM&}>BUKεE$8\`D>a(G? fg\ތIAIW =\[/:_\B{B~ie[%is9)\U@;u>Jp`Ϸh r~+i~'< ʰ-3+"91@cl6Z6trY-vSP~4OP8EPA5{&=&kє'vꠖ&,cA Su:z4>wx}?=YZ뷨^r+9)w'z'FEwDfl'= BX]GR=KH^C8rT_y3J3[H%[ ܑcW`2Ixir 4z#o*)>}0m_y`~bp+ی`ŕ٧ѻ>9VޭY#ȭ g{I<%]mY.o/muwxdcŤJi*~AK*֙:e$@L+paJs69zL-Z;pW %*D뺧cS+rdܒ{|.0,έ&Aybn[ ү0Y2s/73"b|W:0xS["lC,y=kU ai$;Y!$;, yҞߖ-BxL2RMHeF}&(POFcXr.R|#W'~U,N5g=*o:g{u|Ͱ8BK D0rC堖7*"ϵu\_dUe":Em$?#(3 4g֞5߾(vUvбjH:օHs {T= Ĵ~y_&gCt4O"WF޹Pg멆91O{[O >hrQamAcfQ`LVC(*ձM`{7H!;@dI& (J 'FQ!Gi*6?gXzEo&ag*^~<:*vcr*{Fj&ޘ>]W)#BcpMi!WpC?(ԋƔ-K"UVp˪.Ҿ%Q'&*(_M~֔m/^KJ+\\e[wWuAV~P@\&HԘØ! Z6ȼKB ݓ`Mx,ۉ] Kaw|muIUyT^UnB7@V+G㝋C2 Tet*ܯH,bWŬ~A*E[3401f&"rï <'0~ I =zx0E ǍrRBĞk5QA(}H^W[*XtkgЉ}39&t#WË ld\D"\%mFPG?EcB(l,X#3B>8zs{wh[Rl`Qz||C].P^͏Q}C[t@)`fp_3ʙPNyWr.}\ 5L KZ$g-(5i.Q* D&L~ywEcU)ˀsB?MᾖzI5&Ҁ[y1 ՙz(o=t]=R}iw':ՙe~[<c%{gQzK˶J|wMHCT䉟~s5P>n3t3K',U(BlR<u҈8!rϙpI^V;Xl'kV2Kf{ kV ZOA _W?W}v5"QŲiQ(NZYJl%3:/5ػD9˗."UB6^JoXP(R1ײݸ/q!5hHZ:<91DcmU#,;Q) O%G$n+D9vgz{"oQ%L`K(V(; ;x{!"2.fj}jW3F02)r yW!>^|]"PW!R%а͈4%;$tcX&2p֡O *BX30o Iֻ6+iz+RZ[xgKnBy[Adˀ߲ Q}] <D"D%TYSE;{#yRvB˝"^ mPKkKMۮߝLB=JKͤ>5 YeTl-UQNi3%y/ ߝ/{]Ƹk D(?yBvC׼_YKh?Yu_X0f ,{9Qd;GD[j<^g[X5gͶrstYWO@J 33|O.q߷&mLKsd㍾}Bnj5? cSEޒl%Wnyr6q!cW;D~Rx,ִB*U4=@C.B qM$Vd7@邐(-Suv'])$tdEjzqM1HoXVHQ{qُ}Zdqh06D7tҴڽ|69 ɺy_St;_*M m"N,;2Hu 5ϨD۷#o`$nDܲW) JKL6?qc߸:5+ Ɏ.w&O5 8Ht0PMr73aKf A$.F7LNtmjذo( ]U6>A畝(Q~ȢruF9Cg mLyZ#fvP` >[Mwm9tx|g.=ׁ,e~@ެ׵[hAήu2SyYӍhfs!l_ϕJ\fp*#?PZNHN,%ns 2xvԍh޿sHZcS?̬m AR{9yS. >w^p%WyNq5IeX)FV¡x)K<,k4D t X?rXkB]﯍0 K7A:uTpY,>gqKrC {cc|XYNjcR]jD/Cs#.=R[Vo+ Ҟ.e"Ɣ%m"M'E'&U"와I*Bz Mq?fCU-D'"uHnִM(Q|X"}'mpįV{`HGy14&>%!LD"G-$5CJr ^wAsjQ-YY/alnc+E99<;Cfqݹn[d:XYa'+)uWt!~V /͈ "YpndaJC O e8CRF]`uv8_{].P]9Ԭ᫲Om,}AF>ٗasxh,_L_[R>a5;RҪGǪfbak4UJ1coN QPJU.-+2|S] Ҳ9X8 y7=XE1i'&jp T<8rM_LJx9 '5`99,r]iĨLSU8OpWT=2/hNVBGr#E]0(囸6Ng!++ >ŵLʆ/SvSTstO$}\3bK5xIg.Ikƾ|kѮ!Kc FTNUU6|R-=!io`!;"/ v/OpW*k-faG@nArV6 &Gu`NX|%1[אU@{+CX{ҲY&Z La{ lt1d3n7wxާr [=o de]oDnpL%qE䁝ɕ4JNJ&䧅j4IoQE/h ̷iQnv>fhTIlS)}{G‚◰ -5#NSp8K ʥ:%>N1, ʓk<;];?7ai+!L09ɛ'CCrPh(ӌқE$q(UR, GIDׯ=V;;3'P >49;!FeW3jhkþ*@o7" <3 AW,h- @v(xVCϐn ~O>>Y',En\xxIeMԶ꬟Tt )^_ OYʷT!L1[w@LM#ڈ #eue^YU: \\ +~k/6 O΀I#{AѐUD\䟓G=c&(:~ޛSNX ?tcb ")B:AS9ރ) Rn|{9Txl+4qs?(F\,l]eӬ~XXhz%Vҹaf{>^69!~cVJ( .ݼlY;U7}~[y IQ[K9E-q̮v7쪔 ) eF56&:.@pwTU>{& (1-mB 1ewfPdtm= "a iʾ{'/V9KKC9֟q2xTUO4uξ.THhS}1޺g˙c^0]G;St i!w\&_y^)N[Fc76ҋo!Rq}+ߍsx CY)#1Hnkl ^& lGS΁2a^{G/@^jMWADQph3B?NZ~Ob5H.?j̼F):awڑ5) *ktdܕ q0 c) aTPᜤvU V~ |b. Le s@U>!4Oqh _WK{(7TG[Wקf9IfT \o# )T@j5\rL_FTX. ԓHt14qzIX}bUb$Ula0d5%(fH}9fY1GI)MQLtlɕ5Zb?n U1*v涼oHO.6Rr,I>wuGGσ]u6?{LǽaqD%tqM66hT@YԡJ5}[PKs h/ 4g,̹AQT?~&I%ةF|=!`QRyUud%ǪIJ鿛y4<ʈqK+F~۴s0LJb &-a7G!Vd<=;g.)$e*wN}'*6Q5& '>N?!bk>v~PAk`,hhWA+@h˲)}cqlM*27%mɢb^it7~FLBV(D`H^H&uu/51nT8p([;+σS |,E=NVҀXү(0N+($X*%.]3N5BhCe*W'9=/Iک뾮Ivfg.I<3<:R]9BaZ^p@KNo|Ɣ:gQ4vXeGfYΘaJ?C';PQHRHd?\O6%AkkCA)Gl\tnv%!a+LtׂNe^>o01\b7nVlgй0$?4yScY&RPmUyE"K~6pݷŬ3])kE~.b&~W6C; CªYjȸTJ=Ђq[6pY=l<ԺxU_hrzxSF`@3u2IGW$է][ͨ3G8BpG{(p"B1ȫDId]dOHn);g0| 3dCS*)VaG ߁&1C_fp$JZb=ؙ])>V-^M]ݩsVV5/pvx+G+ӷJ:U^ ܐXLrxuR;F9(P C_߷'st6BRjlu~U|f1  "VMBtsI=9O&Y BC/}d\-v"yu u,M3Ab\d6-̇ ~Gl mDo݁6L\m|W)[Cr\ի R0mc$`sH8G (l_Mۙa }Pm܀1ke!`axMҙۛ!NQ,Zާ C%X̂2+-clkyN}ro*H܈6]hChug,AY9PԵ?Cyx njF8,5#:goQ廫m .AGX) A7Vv> NV24ȴo; bp j^Bi 5$>Io2-r72OLo3!)j*~'M8 bD'YzB;2a#qos4w7WރT;iV|ZJW=L$*3KhD9O\*Clkj L}8`*H- ۳upK1,:,o?/+MH3C~E^5q/4?^ͧKmr)px)㢌WӲM AHD ΗJt#Zx,_eֈYϫ9G1ErۆV׶w9慠.N~ LbxrW 9ߋwuh`S lל5& :+e h$EuѾZm mߺx%H3%7=! 7=e~S?yB1 c`'Ib٬,k=K%; :rs0TIĔ ,)3F3zmFn_O rfh}:?pQḰzs4,EOWփl[Sɟk9'J.K֠W`ܠ1_ F4^>'>Kp Zvr()=Ek#il(Be:}l %tRPl2:8#NLxIY $Om#+UqݰLsfnnC)w MAjIm;c@לD_ wc̔iѿ[[A,<ŰE(WKUNiVy>B Ž7:\W+vk NMAAhX,*Hh>قsIZH.s¿ttfCL!!t $'ho9Twm>c)~Bݠ4HE Tfa(HA]ut62-܆kPZϭDDTOT/ o3ȹBOp'6O ;$!_n 7;/kXDeNvZ`aZf߶5u -cDc,RGqdAp縜(5 nujXy(Fp_nB7?^g1Ko) 8քdu4'@}z÷WBo LԐ~]WCu;?G~>N3`p)`0ŗ380Cz"DQHG1nۼS6n%D5e)/{j|g86;>|])qV̿z*vb__#GQ-x!?-N?& HFǵtGB*(M/SGb׭t%#T=rysGxL';m,"wϖ)\Exp6晏xיpIeC@Ll2WK>B' Ӹm9S o/:{nx~ ~(f7:ve!@z.k=|>]jBL&ō.Ǖ*R97>5Z0@zY(~9 1³2vL1yQ|t<%zy\ASiـf*ה]aU ڋmRNa-zdܼDWNQM(<O偊KHDr{@f:Dmz'׏@{x4I'CsvM֎w `b-ԫ{SM< jś1NAC^!Č 6djKƑ7S\z&lʡ{=aT HS=VeRQ~,XoKolsf+W|ptv>7$7 6js.4pۻcҀ,ʯ}Z $Nٴf5>/A)C(f9ͷ*ɲz$zbZ"aXڔRV_U^pN\vzע~34FW*,@هVa!U~(T7DrOtn !J, FF0=T^Hlƪ:y4V %kW=ǩL|q|h$A2/r{yƘDu'w-+ky 9+&MD5?A n/liepꪏƀˇ:/W׍Ba|[- `Ym`%=e RxN#eԮ}> R @5S#asƁ&d(і o0~|;wN9g^rAjTsKQ-YF[[\r95+J/OPAH[.?gL}! J.B, !JoD/u'XVXC\B]~: [źXb8 %h69znuA!ϴ8sAxɼ~M. vödmUͿN7.ܼ|͑frZ@I J@z3&Ѥ[`z4˂ Ge=R-!:~XL9X6y̺oڑwۆw]^{GZ|\rzUiCXu97kTf~ԅ#hX̩֨ (6i,i'. .ھ~r\+a`l*S'^TODZ&I,v,&rd3~sb'xJv4IQY<,XBWr&b(Hdu!;*0\Bw & C(.H~ ȝk>RGMsmn54S3!2C^(1T~(Yj"j,rGsѐvDhg#Tg蕃#(͖*t,qvM\<_V7/K$fQN#GA&Ȝu 'bO]uW SkJK D7Aa}@n ̚YB6C5=+$嫸Jn/Eӑ?W$N0ԃE^$k^ bG;NHeoVB7VdGscs1]>XUO)*ftq'{|t\u*Ak*älwًOUm] QK*—83l5#BЍ Tl^@pnmaLMjb8B?әRۓC*a10KeAtv>o`W%l82ށg F4[hY$2 ƌzXaھ=:'`vͧĥ 0In˶cL.RrJ3=g s=,[{Bς`ml|>935͝ #ebIZ]|k9!L?RkRh]SlfAlDT"ӰTD:v·=bw_J/: ({'OyG# KjR̍[:׭(Ֆ-m .p{`0 g +0RǠPI af?%ݚ.3?{[~}JǏ)ֶA٭z?W3c@R1w(1Z$A$]}屻g|4AS\bעGPo> ;P y'GB]DJiZ{wixZ$3/(n+|6S5p05 AN׸8c?gAH6v.۪݅#~EUT;1{<%bb *V!A6p^jL{ƆTmd EEg ;>;hX37_GR忾+ :Ex*m س$ER&b9ZW/ۣ̒hg8Ũ`u7=H\bOle5TeCw/O E,u;A wŻti2 4|>u:79npYw!Do^gAK }6x K:Z",YZǦc~S޴gS0PqP&U*E>,Ygpcl1zG;CfԩRO2q&+M#zE{5^t#ƱkfZdYò@[,f}Ja^zLÂ9%n`{|!<簣(F=Traި(a +6%F z/EƖ{BJ ŘW-[æCzxwt%HX.MK|$v|Q"5/̍XGz\Ī4;!ƶ$EM4Ƥ\=KE(ZQ Bpvz)Cb225IS˚~K;^}mUцPI=Ye^57`C?~-^};-gH6F)y_q[@U\/)t.D9*D1{_+hb#)P)2Y$,)I<Vk<;`d}P]H/H1/#Uߑ5JHƆU!wpzf(E M-nNN]8-hNk1J7FX۳#R#, 9h<e*!Ywys%i@>+6g6ZDqnb,I̗{i]OٌH>oTG;fEbNOLK^+i>.dY-^V T%Q񸺭F)/ Xsw^oA>O"ȏuHo4%P-y!ŝ2}T6U'f FbpS##(>^'3 {5fSd1kolZ')3 ٦"%+ T@]×`.a{R }Fe0L$xFg} AE!̮PnCG%Bg#.d "ñ\|)0,x&r2!ε1p\AVO)cr .w6y^/ip&@Y}{Y<=H<)+_{%aĒUb8hX\+SWi[䲵e??҅?o]y=Gq7g=I \)T\K)KX+ź4J0mz԰68m9V_q03R]07QsgbQ?CN"7C&WkP$"׫!䀕0,H2;nqz/E$H^hXMI^*CK~aho$݋sT3D+w\fm1e ryZь౼ZAqYUzG,>:Tih2\ٲZΟ;m'T&0Cb $]G CHr/HXO-uRqKz(}Z2n&FcVu{6g=vTh#ieeouԺ)MӹCpQ"lqŽ-O7{pMtirQ='SoaԐc"HC1! V@P E^Gّuzyy4N´vdh0w5&&; hQrhyaGc/9~މo]G3oVvJp)+U2j]15`}@J-@sȻt0kP2]ɮX *;^xo^F3lTWZM(Wu'M,YDȳO̼)R?6oŰaCg5hF٨|x uPfZ,Y=+}hBP$%[~SIh%j}O/e $~D͗×d tu\ + RMWCu38Urz}SY%x90{]ޕdzdRYƖij~/H~~ #i&7 }6\+%mu|crBᨫuἲƂᑣ%1b,}[q|@< U*xCbekRw%pjmsol`[(YSD!/>\LC7,|>MF CXNp=׷=Y{ܪ{,9,^UJij@c:iӔƳv m3׋lpLÖ=-Ts[R;~cntg--lwnjyzs^0Xi#YcQ=R_!>N1Vk;hgC\@"ڷpM+o-fV~7N2 ~#;mdr(;\;NJV7AFz џ"KZĎܽBb[=^guEܻV7ow?V5(ϒB XiR"cO%)B ޠn.L ot!Hcy;X"Nkc G4XV!t3є;uAEdzmJn 7"va%At *Mvӻ#< SG)iɹښ$V:6Խ;E;~{ 1m9(0*(w(eKlRC66jЉn}q#ms!~Y{5~ARrml6gU(sl"ظ oF } MBbx70%I%yȊv2Qӆ Jo+E(GB%A;}/fN1jNJ|Hz'Ρ= yMmR*F33J-0@]d-AWQQ)4>qwXڦT> .@Ԯ=w*]oxzάGp&|R{pVD4J뿠ױqѱc3 q9ѭwhɘ㛓4̤ypQ*⾃/9"y o78"# Pzݸuh %[QvI~B;q[@춋Y3-QPr nw!}g(klp9j%"ʙ*-?ȳvlʿ-.\pjd^>0u `}, $_5wDsh.K4 vdD|^U#Yrb c fɅ[Ck?l$&|оDgu$oEwRC ,35;4']YtTl˲DYƯj[bT,B(df3~'so+ײ^/&[q-R4KskTg='$߶ڗjSUKPAջR>0x0.ŧtp>*GչQ5K%2ǎ^8kʀBFx":ns1hg7d}Fܠrp}/O1ۨڤK30r;Zq0bPsIvcЄӢдKܐ_h̲d _c Y@MnǤri!3o:ůcd T*(UT71.9nrڊV@5uz&ڧ41adz%.UWXaFXXju$વ,]eV wy02Mf!yh/2ƕϾZZڹ=kPG(ZE4oI;5d2%&yd+^ꓒxyğ`VP(j~FR=Nl Ud5>fiie1@i^&\ɤa2"c30"GL _~ǨuB% 4*U?:1^L+QC s*$a/#xϥmX9pzlƸWDoϧ"SO?1~-~ d Yb߼(y1{kxAbdu3f#@h5<.^1%x/zdM7 ((~XٿD\QTH7>)3|snu~(3b?T}Y&446|v6n/ x$+Us{ӧ@w4O, {>C9BͶ)sg@#\WS|L{عli9 G tW]!\2. f%[Dn>6)6( ~x_M3z-A떔*̓~)<0q^XjcU9fz h/(3P颶8W9W> rw 2{+A#&z2G)Z`*Ƥ4ܱK!kޜ>H,{"c Pw2RCHi@Da4LE<9 #ٖssg b쮶)d<0D{n"8%RwqtX#|/h4L.a0,wg9=cޗ;:^fmE,\:,Vk</ ]+R`஦*:P-M"bJ-3к(1>vNd(֨grL$Q4R,GT_g}"QfcPTt2#whb₌nZ//ZQC6*#C- xlsfL\]M5OgielgE2Vkn6,y(D(V0!2( E$y!249[*(NaU7L&$7 V OC陣DHyFmI馜ϨqGȮ+@R,6FUcE=yMP jזbycҚaZ@}x)rdєIC7>7<;8yd% ?{K2xh e~}>u`U-(KknK^Yϴ t_%jUm"Jh.?V_˷ z!Ů얀J61{M;3o-K| F{[9V\B x$Ld^p993Pjurϖ 1ПBu[]}wj7I~G+}2P9(2j W},p AN-h>>-p%\ z{$fm|U\o ZMoXaoXI?E4I6Ә86oc𹁉i Xׇ c@eU@aMV(Tok{jTXKqԴt3 ȣ֙D>feıYH0~Q1Fh vö1GnTq>Wgt]ַјxkdk\ E-6_cv~l ʳqJZ#=I--*l_عD닱yuW[Ц+Ǽ%֢hT_8k: CA}k[@Lz"=?U lĉ@M]M7lq!;+ /H4K۶Cª34zOb4:a*C.`-ڒ6v〜#^04~xO%)Uۀj'%e+P$A0T%+vOؘ#@Tc+GCRU#fLq)F8$(7+>G'`Eh1!_Xmw #gA^c]uECGw5ڿerɽX*+H%Y;$ͯ4(9ɶQLϛ)RHU֚Xܝ,ءָE[WPO ?ĠvTzUeO1x;/PY`=R" JmI36>X3$F< X Py{)/C,'08/Uҫ9(H`Ltk9?fwtO×Ȉw~PefwCIfCו>2}"N[ qN3Mso,5s~m!e~祖҃O/o3;`=HQtl;J%SRT,3G*m<HnɭR2>֎ȔhXpZ]=^,chODʩr[kU$JSu4}]ގ'rzӾ8גNE}2]'fS+NΞ aRyeЬaE^[%V;a~ydj|#uu)J0^R`!t!~єSn.QOs:rNM)ֵσiw~oQugro⺎&-~"703) PhVWF@d[̓bTvNST]^=xŒα2 )y@OqԘ[9eRUUkR A܆Ɲ&965rRL4l˝Y򧻇" `rb LboU% $[=ةɊu.oiLv)xF}A26 YgyLDM86ٱ 5br\}-YL֙s;Lu'-N`?8Dk##ԝ(3Le#ldYNK+8r|_alsRa{dD}c} 25TF] o:Cgywt3<T.?umMr j{i%+V5:]xaj1CyJ<IY]]/>5*-LowQ^)`ncpm#KWhmȷ"sz b8@M+ 8\Y8Gy=o+6v)?i/F0٘EKTRد46 nM#sD*v2(]9o86\5dt[U9o߷qr8. aq3>[nnQ%BjP4nAcfD.MdE7.+cĂ'ƯgS #fդ`Uqntg+Շk{RaH"tr5sq.OsnJ0I(R[N;y# '͚A::l@`x(@%(Huf:0Wnsa;Hm7$ZrEBev>1B,h8ƣ*jt31VFΫNHp8P[O?Za/H\wClJ(_v"AnB\jnS^qʸC?$H464' o}H#\ũ3{ ^'Y̵o@/]~{LmEz)@~ē) ;pj %p-#gңĽ9ˋr\V mx #I_L[_jtHw =3Z>:$e{۴ȧբ Iick cB7oۭT`!%]f:{k 00Sf 6KH A+"GE*Ub.`^)1x#Bp=[K\PvRˊ1k툷5}nV7)fQpRy9/.ǮH*d’_ ,G&\ $RÆI'¼7LG-#pܵ>nџ9Boj\nz#BY51F"-<:;DRZ[EC'sv -Plnnt0Qj{H2I5 ^l _ ' Uw/cl_Xu)I*;3F/z`Y< Î[wen^pο ַ˼/iY1*sKIlf>9{2' %݀[T=~)*NP(٭[6n+{]^sMB?p& n\HlbMo.+3iUZ(u+aɄ1HFMU<#Bb0xUr2.EEHΫZF~v*N82(8?0GNa^)oC'SI@PrmP-j.@jA%Nlߝ$9D{KL?jFz}UgwSU#P$yM/DIM1Po7g=9oQiS_k 'U^&.. _>6D^uq^D&Z`8;Euy(ŵ<}|?Q6/Yc%u)P` @bӚ[M\\([-M5V&PW1Tyn˺Bwhxuylomg+U7: apu١,0b'D&I1 Y#CmlR}珊i8Ą\01$G<~hI: 8PbQetנ?GAPhv\*pIh%m%8յJ|\k`ɑ'yL%sl . f}8$m ʬ6Iϙn"!Jit[\ rcXR@*J q^":4ۼɬT>0'v/^ܧ)p.FG~ǯH'ʈBQRL!yTeTi(H&&_gAnyXEOޱ7[=6s~88z< e[  4LIk W*=ЩRC6vRO>dfn@&OoY\F8`yOsv+T,&i ](àP\V+&l^Yn妮bT{PWX4Nz+q[y֦C{Pd r*Lx`88Q%|NIKx%T;]$tcUN;A.B|"}o,UtPEzlz(SsT#p@ȿl~MK)X3AqIS7 fHmRpu>met7sHէu<9syܡUAU-lݠ7=wA6(͍B\A4t"JEmfBşr1˭{nl=`nS%N6[_m wX4IZ4+ Dtf=q{ ք!F$K?i_>C t ad訷yc(MTVqQH6{a40RUh-9\ڠ3 cc?[P)g=($jYHG>X1g@&bd@y?9vfUy!St5C㜸=&NvQ@dyeN\szy *턟F_RF3Vq}"kVzLNh_՟n$'z!@(A2'ks- _RfS: 1ecƋE)P DvjсZ;GXSo6OB`<[ܵ&VrF8%bF=%ndDVIqIYD5yt?[N]w gڵk=kWvvwg$*zUhˌ=ywItZ𲧶}d1 Q󫎌M.HܣN g'_^SvNN[sfI3U3Z.jXWR[];ՎF)a/1QRq/wggKv+ܩ>M  I.]@KvDNFiaBKJĦ fYc."M~ g?]Yq(O0s[lcQ.h\F8rs70gص-6|K${< '\vDF,4,8z#S똆0..]$ؔ!I]OVRnP+꬯-{^hl<[Q/4IK*ŏMdQdJ'#gQͼ0%pkU7؆(|j17uޤrt;\U a7 Q>O;ٹLn=ؔf6 Mu=p3_*ҹ23Q6ĩY!9lvQ$9.H:,J;Uxaoβ&9ޔ|?ՕxSFQ]A"\u/4|{>$tvΠl.r8mu8cA90k@d֤y@dSyĠo9l!mds^г{.]͂$cżvAlEuh3~嗽[R=(V[ѰVLT ; ̦ Fϯ.sÕ؁WoAR] =E}5~fQ1٘jBma cgrݙvHLD "-hIڅ+W!訫HZ{ T|gR-BBb9nXOZȈ 15L"jH YO>Q 1781~;Hۓܶ+Q 'e`djKd R0ncWWqd- yrfZ QoqZL2,Pr^w*~uoR%q2裍R @Tv|k0#9,l|朕ąj Wpyb3PTPTKhx)d5 _%hA}ԛ ؤn3e26߀@ {x7%? wi6\އԤ yfr_WtۼDPoRp땈O ‰7_CWIep(vߍוG$6vqB'킁s%, a)M^hmYLh+L?tdFPI8w % I'9EBh3~r0=O65sy\q! %inC*|SyE=텬\EQM'tXO~K 'JS%)G h\ K6Ce| reyDܣL-5DϪfx1Awc)FawAAT۩ΡKd 7Ϧ4g. !,ZPǖĐwPw[8eWSz[0u`a =J'O` Ngn_+yu}?d]\F/:'/ ݻ3ʴ :o1&ֈe|.#uW/f9P3ԤE@6 YT^OI c2ћ^m q'RenZ1ЧE87'|DmSq1c<('-("ϷfRYˎeIMX`G)zGy-sRN(v&bG؉KM: H"mR{J@:_̡p^=(Kʦ8T؀b2P=8+!{ҁֈve7&ACR8+o]m;t\1T\H&S1"AYނc"5jz7'ҵf1jWPWuۖu@x]Y*ґ7ۦRt~/Wԥ N`ӌfju> ry%׼4E峎mm&Q\Cz_|˘CO߆p%q%^U~%wv(KqJD2`_|HObټ|  MAc!Hq;Q*kS{r^flzxo[`AD_; 9<ƺ\zn59 D> ph&g8 @y/2*[NbQN8^NrEӤ%h!{ɀ0 X&N5HVW4TL۰ LJtz(&Svx;f8PU@ZH^-O^cqQ(,cw0 Y'`}Щon! WrLc_G +t@hQ>AH-Ĝf!\VRQ48y6&\ ̀zz*sƾzNcz09xR5.m(w َU) oTj K WNkb ڛXSM9N VRtL| &55t~a7 vh]TsbH8z)wsܒ|ӥK.@M7YJB0*ֺRݼ8?5@U`gjjq2W™l{D^EҤcZxޠSi]I  v`4X~FʺJXxG. %48YL*~=G,GR/(idrzdm( : ͎\]9%u4qެf]"D52np{t@jezCdcCQb$5 s=6dskRbPNXhV`">!<xQƩn8Qy}:3G|}|Œ~t9v!(Zad~+qW?2+\|y9Y}._pǘ|<2%;6M4G)QvcP2cʴ`r9_|w*;e BXQSeT/˞lxy].;Q $[b5 x_= 8f  ]Dl6F/-NѐS 7D)5^lM8#>r&SNDعuVh+O HB"c{R#ZvhGzSޝWJ2W} { d~:|3p&I`,E!]Mf"RכI픳z2#dXz{F;Y>[ CnPDV7R[#k&$Q%DXr j SVw~$DsoNavsQ7 y¨Z }@cI!]m$:3͚T< 7DU:*7kx aG0i+s/T?[~4oeJv%+` S/@$a"et|^&nQ~d'4ky|]٠Nq;g:+ TXiG*wi+zoUέɑ "> -Tl%n{ }K>d%XTxYL01}}H;#g e:T$w OwW?7HA-m[{cM7LZ a(Qx ʁE6h7ߎ8K!&v;. c˟ZJ{o"PR&m-Tq_QGcBʝT*\*hQx=QdrE*Bsub pCzZs%JcEtcw6}E?EB֑ w <Hb&a1ޞ)M]t8EE-yWXMK})b+hۣR%>ѐ)4@ΧVS 9&[H:3(bBu=Y`X$2{ōXo9ɛ Y_n_OݱE r}qNl[aE3~bbv+.XN3}'(O|YZ3s*.DELn+ qT7ędy}/wP[.,!in׌uX3[g3ܬC0!w GB!]X^;mu\&<5/#);JW}v_ѹI<υ#"j \(a p60 @bQXO6s?,#_rњZ<ȫ-Vrl6g7(8-5h3x= 5r|]9P:IxO`P -v] bVmG;Oqxcjl)<Ǚ{l42w:,œ>IGG[Xl%nx?WzUأ@x*8^b>a!5 ]Wpr+n_ȳ_ Zf3 O\IMlܧN'4NJ3:ԱHȖ6{Ip < Y'8z*z?`y߈k07fdpIO* b#cn#oA jXx D֑T8Zx ky h(^^*4 (_;"3q5cʹ&ˊLs;gF $tUϕF&i|VJ8X!_R&𻕛fe@4=iy39c dD JX&.wo`(bW $Jy2:cHM`E"t#m} dO"ۗ>)T0|bH[(c׿\6 WFkQ+,1͠xk mal6ʥ]T]\k:A2̯ N =bj~ Q9A̦ Wo㝩DOcc?22`h,ږ@"0Bv~'Q2o<X%UFfFo덌/>3f|~j|mF;wu#g|/3)vdzIv)fT*A5BXD5"jt:RA\Nx_R iRv@(Ё9aUo(FDK c"=iH>g"Qv6AUB5끴ب~Br |_Y;3*:dq=EmMJYtwe]jH cyT6bQKeI0e [}7hxE;b$H;Dד&f;=f#SLեȚJW&@zIz0"x 0X~2ќ:m W5 Z4Iܳ?c ozFŭ\[,Պy`GEPFfejsEdQŶc؊r߮$iPQнt|{{͍>=#O W%fHT^ T~A_0, " ma#7oT0z8ps 01zlaKpbp=ik`jP>8kk[!7}]}3,4X|LxMOk`: r)~zg"d9}1stl͈Gz&&^=#C'ŗٰdn'\9aЈ,K" $J$+7{F R䧍jJ1ܧt!go#wrA7^a^-bn9PF$T D/Wtﶙ1b~c'zRp<4HYu>cZacHaA6FQ2.fk{o(|@AIs7z dF᮷(:&hS_e^aEq¥ +^ы-U91ޜ D_lx;wx1yCϭF{W~\({e {LZ{3?>ڱZrDQҮ׎P-ZC11"2aەĻRj@j}dR9d,MIIIcB_ "zF"O T]vBd7aolxUi_{aWi;7/Mi~ 'Qi0Yzre_ s+\7Dԏ!)z^aj $ u-zЋɂrgU-E!A+ 7 Zl~P\6s2$;bOڻYMO,go ?Ș$?1羐ӷsLp-?:{eH6wMQSR ϷlgN,U<Ѭjf,gFif[S^qlM=ƻ>,vc%|׶PY3B ?v8AkM|e7%TWo{PŇl 'A`0l ZV;4BǙSfCߴI'D| , Q 3O k KL2!cp-n0; OMu!p3T@2a>1e4(Y"TUUj`;S~Yed-xt*jV>Q$Upxp x|ݔ>J^%*m.̎Sܒme8e=LY7[*t#FPC.оJdުyb>zY;LpN9|sN"đs:A(Uqm?|DۡXe{$j9fqLթ݄* X,f1O:C؁S^qVֲ-O2=?/3'HX'g%s%sK^1?!5aB5$0$P\z=斓ĂV$Z*CKr4n2f;EYu"z>lWD4;DTU1eG)|7oW9>j \e5G@q ۻ uTDCfج_:8^ \>\u9(B0?+D#-2 KJGT_9ؐx+WFv?6mqvRxnw \%2r }YeG\HA?<mPV&$`5fFf4rer:@qWِ8IV d¶0[k,Լ~ĝϿțNs^5]Ēn6ňD޵ƨ]fی!g#XcPz:tuĞ 8i/ M.6|6JjLC5 m77Ϻ%CHx&/ ]׎T %jtY'bkP7r3X);XPIeh-z:gIa_\CFK_A.H`~.UJRi0f'.9C;*=Ĭ`N5?I' 7d'Ӕ.+(yRκCU:V:iý>v)cb H?hyYj bsTHb4e$pFKTd6v.7/ '!rF8Ǫ!u[n%zqɔ: [їf| /pn<A(p(]9@'M7O}Ccs+a]0\ǀO@qIx;Qv? ɘb[ m\2i1`xHgPGU%[i^sZP3c$n\2V9q[oy7cp9%,c{E"ΤX A)-W%-b\q%(p'P/Z_F0MBXi=Թͮ`미;Nd4޶hF#l3!vt%~:-;u ÈѪnڽ$Dԕ*v+W񁱎rl%7S.xY.Zo00O=*`N_HIoYR8 +&xrhfXZ!hT9ԩ9✠P%T#'X0n>.詞X%qkgB:&Q鰽WguyT ;hɓV)G'~IVB׻˽b-۵(9KgV]">-ԡUZ& y-/'d@Ђȴ ݃L@5NeQYW7 gR9#Fjqm Mșsu@[knW+.T]Aͭļ7K]S^d>  f- ^8'p91NҞ gik Xl-US]TxC;WCĒgpHx?V5 o !y$hpʺ#Uv ]Bݗmq̾wҠu`i^ h1F{G@ ~0y*(G\~A{"$:e6+9vn9|ߌ2z F&pD4 ̙o1*OA[)GlH*rol?_bÏgt)NMN l'VԻ2hRq!plQefB3K~HZ۸ 3fZ}$S ǷȻ)QM_w!⁶;wb""w )fo9%"Isj@U->)"|;;}HI$[9t+$Md Qt;Wfϑj;m& Zt1^"enr*ph/NTɸ6S*OpM5CS\#I {!y&e:*!!t B`?OtW4mPj~]hyDaUCmT`J z]ʳiYln+ L^kM*9#w {')c:%5_.䲚вu:ށi؜F`Ԇ >%}LPۊa3a7EhA<;5؍m''&e)`2zb1l@rtq9ś+Př^ ;6|"7~٘r|45B)ECBvr4A|,??0Z&:oQAKD*/x3MN=zO8fa Ղ mYCw~1U||~\rlMe wKOcp tB7 p26mPXOK>#5V*nNUF $vxz_;YL_{h |tya=Y= =XtfO7 41tEȍYF[qHOxYSZ4bٰ0aH&{IE6T/%lsɀ$idz٠ԘÝgȍ|).HmpC܇Y}}JWI N3V߽[s[hӷK Ll aJ0u8,v(R?qUc+jAYl],QHq!)Ak4KVDoHu}י10mڥ5)p|)2l:LIcқ;xbu= wL ǰw[(cea Pj&<O4d@"Gt($vZܑBƀtwIPp r [S5T2IjJuNx`"ehB~T (FpAYn\#JB(_z/2 bhpWb9g);LJwb]KqZG%^a/؉[]O/Y)zXNt51d!:&F`={: GV%qM F#^u{}?]MO$R(K9܅ gY%;寬 %?DdUsئ/f=< :~BwFa|9`Jw[U@DG'%G]}X}t&kvKB%4ҟr/̃㯨7wA09ʩ@骩 F~wOjV&$:Fpt~H(Z㞲u Zg(,}~ٝ4} %/f[vHjA6Lo}Q QP>23RP`t!["GWu^]Vu)\_}Ln)譌X}Wٳ10}߷<܈q$?wɈs m.eFTX`֤ᶓhvL"m/־*p D]Z%VbH$PNܶdI "61~T7=o7×O@ѹ`3T6wFT"ty ᖵoJ޶HѡUz&Rgu>xje׏0%uXhS4O6{?+l1(8<(E[T,?u9g<2߅acm #8W]No₅t$ b!S$1ntF[v<1v`+ d%_1*s7<[q\orߍHD^u~ʧ@] ,ß \nbCui NrL18Xo^ѕ'C5O~Y6lC Ӥ;\ȪN=$4NZFu0 F({v_*wq#,'n++&TH;HoL{FKbg%F2O#5+#m b/kBIrG' ;j2:^X,[2sx.-1&{Ņp ~ww,C?΄$'vfhhThg`F?CF< OӭR·MF%_gpcȪ免 M "N='_HWF 8ut18iCg&-> $F{q\o#o7^E#|`nTmF K5s?] %SY8 @ T iib+51@Fʪie?k &YjJW29,wUri>ө3(-1S7ƃuUܺ0$sfPW?չ=pfc3#KG\O:F;wqU8MaTJYՌIAWo;iJr(p%RL.뎤 b+8ŷN!Nd<6K^*<"lzqI!̽ean?G Ʈ K< *It lLAעV^mMLe`cM!ا bP,Z𩋃\}\#jm^^,f&x*C(2qnIIfpgPGV=7%6s!p9LoqΪɽͦ_j\=]7ܴ>5I&W z uouusx5㢾Jez/S;3'=;o&߽{-vA=XbP kv t-)&9U"x<$9d` _FẃAaXqɦ6r̽et`S ၃C\HSP= *Pv`^i~s0rkrxĝ4I=w/M"d0]Vr#+peB@C S,&D⩺WxrX8܁Gkajc\}D0`ﵚ{q'Ҡf-(C͔?DoQ $4j`{r93:qmk1zQ%ftk&plUj[~Y$Q AcghykRƳҖΥ~"@[RqYLreÓBwAlj Oiܠ VqPto6H9 =}nje]nڇZ ykN"7nTU ]25OrCsYVFy%(d'd} My 䀀\-B~pj?}5z8T:噵SeuS VdsT 3&{U|n-W zDlz0-#OHILUtdrW*I:()Գ؟9-R\fFɆc%KCakvP(NQn3V@a}Y.~ݲ _t͎꣡\c=GNL A銛/E 56jFYdY'v-SC۽q iZWd`DW]iX'RU,,НjBƬݞ]<;M jbbzENT˿<} v ' U2<hRO3Q7?[}%NM~g~`pMNl< K#&`.ϳ74 ꄖwnksE\:Ui9Y7l@U ?j8ܟw\^C .4?;B:ZdOXh,ѹ_;d`,xָO 'd?P%|"F7xŏ_οm/MJ;&"YM.J[ttb HB9jq\P^I3q5E? u|ƅzб!{;9~%ZPOo$AU4-ܲؒBpmm?D7n> #-Ҳv.N OLGe{'[ջ~`D=Wڡ5y6*GTvK"շO Uވ"m=/9V<$)5.پO=Jdw;@Gp A<0 Pv!VhQ׮?BŇqyԈe̛_:xj)@8g:})vЫ?R  X:WZ'P¯N &V lirF2jqa6Zg}ltӛ daqI/T]cRj{`Qtazp_ N^Q`_s^5UcL=8]+T?!"PŒ-:JUO#?R 弚,mV@ְ˞Y; p(5D |t--jye:~,ί{OWT:pGqcUe4]>l@̖XDd/f\DLbkXES^0\xB"!a W#<Nō"Y8'ef,FNtM'b* *$GEXiwC X7=f4J{5#|~,s=h & zTgUZh]_<ƥHVJ=iN>1FAOQeb !X-u1IΜM,BI]~ '_4yW1B2ndK*iF3 YQrW|eɅ9#5&bߢG{MnU< 2SrJVh#\ey6 !os B~"YۈR;-O/'toH5㼜{vz4}` "8E);wV$VO=`dpJe?xZdz5cq@^$ʛPt;(TR]@ "QbOwiAac'oDZ4ɐRe Dt\w @:[X]}wfaxl~I2) } &;HGD~ƽ42//H`P4#-y{< eC[g|H[`]?e2ƫ# Olu'^*4Memʽ^(#-:WzFLj 㚍VfJhYm-~'Hs-^I)U  LZ*̷SKU'}O>JY9k'J& &@ 1<_./%PM諵5wpe?.(,դӠsҟy\-b'( }*)M"GgI|m| o 6U͍ρbX#uS ,uEœںs悯nalj}@^葎pB,ӜR's3@}p/o`ະNJZi㔷WUQJ$)5,΋3ŰW+zf$ F#n%ǝ UQ*L?zgӣܤyGcR(ZR^]Žzydx.0Dž#X UwXCKhߙt`:AS( /91xb%>h̀1~@ZF4{x9[Ǵhf6B;B|P'$!ᅀL mKg{=1PSq:*w;C?묣 {μE;`7 ?9o݉$XxP3ΓytcUr0Mvd[\@QL۩E:|NKú\|xfX;X]'59U{pġy4,yr݂팊cф2~*0/b L"'-Waf1՝r33YvHÉ_A2J.aZg oqm*mnۺ`[tLnMxҘܻ1Ooy0LFW0`%!hЄ4<'kqE,P*Td6ll .Н]իϱގ0CZ+[~ ^̮̾o9 7*qTCjJt+/&~H$o'afn j$ 4|\.>]|5 RoK-'Npe { {cN݆z66CՅtk #^~3eK4N\UfSZ')dݭτ1A:}!x3f'}(f67 Dž] p1q)&>IG!Boơ='oN /JAtN{l;Ft ݻD 2EDyoPm9E(m+ϸrRPin+M,752Ѷkq$F3LQґ,ꗅr^I:t0*36cG0}53I=ȘqHLCqqq|Pp0zz@2R#9cW3)&ePZԛD+5_ C__~|2}E"7!Vda hqWJA`Q]T@<^@ *bH5{(f &N51v`C(`j{YuhT qT'0Mh JL\'%A*BqkF DGzc}67mҌ&ndiXUZ i%+y)lIC^k#E 8Ey_匸Crz+U,ZyoPvHiM`cN7,.T6`P K82֙ nchJJNaK$S+oQyhg/bbrWmOKQ̀m45NO^~˴W~BpLV~tԇzփ]sǼA p5+@w7OuevrxJ[DG@v< ,RPGhA@+vcB| k]ӕ&E $w0CQpc\*H(󘷫 K}9zᡜW.roy*/woe*S6 (]85dBo(H*BŨYFA1E"" Z7XcU%@Xό6 'TxiHև`Q1tSf/+5%9V @6$Zz8Ť[@YuKbzhJl ?]VڽpIu$@ 7G E-6s]Qfp. 8d .|剜RYDyjQ-Edf0TnmmE8*(ЭG̻kWԜK2^;ݞR+ȱp vIWn%Byjznb:f{ ~yWuv,Չh9B";Y`^t9AUG_g':VN,礄,:|ּ>>ral0^#VRp~|,\MDvWp;=*gny{SC[( :txJT71O4 ?DDn-gQl`mN9Tlh-UdA|r9Rka5[?)fޏthϤ~bď s>i&*!c Ugz;`Vuf=^̃j*N-o k|T/ZUNFgZzgyA&'DaIVd ˀiQCJ wvjG!D"N|I̷oO\)>'V{> Z K-M9t `,0V6WLmvAw# A;z?X2SԛO:nYxc&9505r-?aL$p08{L^Z5Ȧ1j>otݢ=I6t;퍧-.U2;a"8]"bhg(3ξMx~Abh뱚qH[LHo?] 1,lq=D(I]5ڬuiq6n3(0Q[=ĥn柑vyş@QfAR9UtMLd L&_u}8Z+dtF܀ "9f9،" I5kջ#'kC%HJ?i^QoxS_7ΐ,.,i)z{r4:{k:vZWIO#&SBvρWnA[ynMՇVXKnP}ey3]% hK!QƩȴRDۅW!t3=C؋_<f9Oq=swLǝP !ECȸaܮDt CL*6֕Sq +y ptÆ--,%B!M`8}q h:Av$gϨP+r RE?@ۻDՅ!өZ9V#?Y<. ,_M|:"l݆`CHΜ0k^qtN1Ǟt-ֿaxB':#K2׽֪G9--VƉӡ2gSc< 564&^|QfILOSG_! 5IKS`R x6 A`ar2h zi͓K`￰p&z ^&6s2e|dů5~4?W:*5-h mwRTټ2s{j0),7d<"zez |t&Ҵ=ͬ3Ʊ~dWzJ% XIvIs]I1`Ylz]͛^]6!x}&hȟ^@1!꣜>?UC0$n᰽mɥDθ`VvxQv<>VQC4ӎ/=S);}fEȦҫFv]#ܓr*Pz7sդ$zONhh[Q`# 0dߓ(Q7F yÕYvc4Y}n5 nV-T!2`< o[cЊ ptq0&`ѶぅҰZ}an(:kwXf]X،" fPkdjiGo[ /<6nso#c,"_:#彪@ :W-oSzU}7.Q~± IT rL<^l_,%QV}o3f.AJ.8p:GÃQϨg@E|9I{ݝ̓4=5Yx2kL, ؜>"Tg.4~ 2-QWL-Hv-C]'5zd/EYHkeNu! 64 \{|j t8Y:g @RH5}'ae2R`8Ao5`]-+8ϕq(ܷ?# 毠LL2ro#lh&zbU4q?xdJzdѷ]y]27\΀!AL Rx+ 4spFG1$8RRb4&2yN:/u>,k06c2+1P}HyOO^U$+AN 6%xw:1;o=z]QJF<6כ Gq?#jPp*'֭\F2r%-ۮqSJb^8]\P@oB+(%3s3jifW.Uj{[GOP~ѤE>$4Pk۬p\83EjG!W1-G u!WF&2$b(ROLK9#U>V TNl Jӓbf#9]Lh]o w:#Sq"sdk<\Ғ\ߤu!Raװe5kh~)IbgR徟Q 2͐ L U;9 L efoDd\f 48):ٲ2¹vO?RF+Rv& xf\Tp:-g6-!jvF$? 7Odkjl,#W 7]1 ?z:1U~liUѷOJX72'i_3$Ͳ9I %7#:"|n_ L.Ј @ Gb_yU5VeM4S/PMgw&oR WL3<Q,; BΠnbs#HTw dpG'+]p${zo4զ؝4҃L\Ƚ19HΧ,`VC:uT8m9Us]l u'1oQvJ>ө{رVp&Sùb3Rhi; |*[1S˭ e^EV2$t'k3HȪb:L8vaG/Xg/R{3svs BJl=>d5 X9UJ,x2!^G!jW# $KZF mvuyF7ELZ]9@ԐX5a;=0cQ SRQ&:X("j'^m(jHgdr c)$cnIC& Jp-?̗(0V^]k.`Ʋj1J} |$s[tY d)_jyс ?S۵ fzzjdu;#|+ o1QH:ߦ_jǸ!\zMVͰoRn 7nQ'ACo!#1܎pRjWp /v\" CjHO`~Y.*j؀^6/\ #>n>! 'nνٶ^&ȈM_qea?f ֆH,^n9ď,D+] L2 F/ϊZJ::;hfx0_LL1Pw/N%.|td3wFrt::llƃE˗:*کY~e+YlgɄo/k/&0vrێlnc-Dθkw`}/1'Fxy EBҰ@_Y";J$EG\"#!Y D%m֟j%AzNX +An G[JۼEF;{@2 E)!%{03H?4:47;j/ wJ _U0GNFKwjmPPEB²{Vssδ 3Z TB9g0gjҰPKɅ aĞ6lJj);6~'_IlG" Rq_'xآ[,/8& L%N|FȜ ;w 8TR˨TΤ 0 q1٩AHpC4DAe`wSUi#mSاt`Y&<"p[nSƅ+N6;nx1=Z+-\TG`oP&7.*a5VqX 6l\zzǛRc x)Pȗ9q 6ՇY3I,Ii y({}ޤGDx;?hbL(CuŤZ}.-j&fl.n?`*ރ>S(,oB=_}7m$'}7Sju |5剟Q[%B^JI`Yب <%PW w?2'X]ϯz" _tcKQ3-+84kwJDp(Wj6nÿq<_B b-b\ѦJ/h4{۫j!ύ">WF3O$1zE#A m.dvXxyc675d@Sz^bn(PVe+B)NQL]R>#Dm͝qLpVAyײTI$>6sSkJ5z1 nRE`CEg9(Ky6Ui\ k6)8D|J~OtД1t]=S=vfl~&!sYi3Coo.#ow)@WzjgD8RE4ãXє:]:+;XmѮ;V9஖ΨTPAX\R?V6BEϤ_G2.!~`u~Ov* K@|Aye (+o/T弙 zn,jn) ghU FNwj&2i .+ƻLmMKi6㧪6\*vXAG> P![g=U ,)7ߺٷ]^#<.G7iC%qJNFzʦ,!ݑ/Q HpUj V@ݺùU&>T00U*;koG]B|5/dFH{7A W>g[.CiQ}%bx!FxhΣ~ B?+V_[`l#`mGP8R4:g턍]\Rr .Y>66.G%Fd+tO-@;*5~ ՙ7t۳*qE4>eSk"fB!`H8^ht+~9% \1̠3[ I> ī"pIq@<UK"1yFpW X׈ɩ^Hn< uQ-r#a4۶ꆉXKPQhpUK@uM׼6+:YOH(<)*/'n0y*mɺIԬK&aj[`fILcN# XC*1#L@+}NF0ZRr kU P:Î?$K>Ҍ]&m?-<,#0z< `XM ,6% ^ l8:)) hygײrM5, %kUt ItJ3r{[0#$+SzB[ /Һ|]D _ ;}Z<63eYx#kPC# sА l pRaP642 TvS *ϭQ<49Bw-"08ip_w(Ryyd}\}L J IsDjyrJSry5R2FF]\@+`Jv)GuޕA/Uٔ`l*Tv*N^E;(bA^#(H+!1zbNՍ?Ru9lqB.*MֱW^#P'5r"Y-Lr wM7ݳ ƿ\qNb9 %F}tv`7'MHd{'=&{>*湪ɐY>!pϿtuDϢ#.,#e,JJ$ǰ^jbQ|bǕs-9;ǀ$z_3RlψʥkB7 < ~\3A%WCq? !%yu/&&2H1ȤЧd 5QN֤;yPT !> xCa~BY@5)wɼޟ Œk8m{+ #4iJm]!*:îRA@Lˌ+*{5=R 'mv 8XfVesUMQd,c'<9ʑ*9@əSh/%&,㩎i޳miݯFkw!uzBS(C^`=;r5$;eQ( ݩ9!Z=Z#um6|)>+MZM'T5$GE&F\wny wCIY<;Vcp +\!|r#^2;]Щ)`"v1YѠjTΣÝ\s:oA3 ]t|*$f d^IiuXSW؜ Gwy_I9?٣&|noZfYRC IC,[_#. ;!cQ>zس믔G$[lFo[iⶭ?3Xq$+\h0"ݟW,޶95\PP2+_avcHbN=%-ҝފECoơ+ƒ)2f#2M YB8()::~6sY.!46u(d+guWFv,TXq(/ڍAn1 +@H0dVXf"& lU8!>,/mYp!Ly5&Y"3DP*kR(w$-GRe+LVN>Mtx.up|K˸ tF>j5> &+m'ۧo/Ԓex!.(UT_ժ FӍZ|`fJJ2ee ,+Be!c OqI.eY&+rŊԣyoPk=^!ޔS8xMP_0} -*n:9{տ{'P`sQj{X,f ̮2DښB,F;.ۋ}E(a5xtڊ D*:MΗc)ض!wi2Zsw,jg0GbAc1T#{#ٻz#`9X||ќ(±jk0ԦpSE׾RCEuaH,-n)C Ϩ%sXk`c 3?ȸ`J`D,X5qYo je Bq>oVO$:1Z9l".x>OcܭA1MO@o-_&[HQ"7C :{䭴P)X M{ǫ >5] 7,.Z(3Rb$;D7 1H73r2f H=]pTy@I^BJ )|oO!}q/.XL{{+ GFgUQ^D) kc1>'P~I)E7ЎV m& (#IwPZTǍ#c>H}x~C ﴆ T; ^^bڣ=#w霐 OtiDŽqs:<D7̓ *C6^wTTxڗ9$iRg*/tIѼ>&┼U:H.75N`Xxwf_mn[K, UpؒrdZ=q26"bxF)T 1x* -!Kga_Bf} *~ tB2cWU4h= e^{"c^i'÷_*#.dկIi˂ů}470hc@{ea[v:iPxкtf9pE H11R4n䐨}Is)(5p#asSynkG/N{@`N25C29GsI> Օo?O-4F\27C>1)0$th?qTnԃO;E}jژԓ8}VC,Ao.]KXϸz٢E(Wh %l!04vW5Xm^in)M궰f1ٰyQgNF j ONSqZv( 'ɴgHI"PԟrXb4Q`7!>ƦyqK1Lzp 3K+׹BUs4 BZE…f 29GѢ04XuUJ&2{$.%!ReY.gX֣R^`)6! 'g*kAP7ьO,PIL]w 3?nAU' y)yjj{:O΋huoW - 9ΉTyM,ݡRX]6^wl.` $ `iHFm(}|$*H-UD/Qrѳ w50JJnm BdbdN`4co NI\#,fM=} cm7.Qn 9[>QX1}̆Y251Wӊ[ͅ.HPX?e~cpz!z NG F)nR _0 _ ͔^=8IyzΚ;Yvۛ D>]3T(yx+l;]v٠(h:ڜXTjA>d5{Y i<.G]~\dui[ ..GOS&wL7"W[25ޕN ׾(OvZ;c+s0qB,)Sۍ# O Tz6í]TNiZ&(uf]`RI9:;$[+ Z`B겡⺀ I6ܞ:s}Mj'VhKqpiujj'?JYBgNڂdϏX;s9: l1\!:2>#B>$>uwqx0iؙ*RFCpW4Orh[G;a Z$4_Jg(+m0\kǽWlXHss*X?ً֛ڶH#Mqo_~?ag NDiTpn\*MM{,5VK9y PЀ7.;!_-c\s/?Ms!eH*ʇ5^&4nW"bK#G !N&9~W`@`x3"X3 AK>i/Oiڳ&62Im#z;|CBRvl`RCsr[9=GI֗Mɻ_zqkWQN2ٛkߠ E^K2wz&Vn)+p:U@B}R~D驢ԛ-zQz3OQP") T!Zv:k rSFDrn5J1'l3HS8UFԔD}I+F9M bo|YC&F-[ɂgYpcN Ƃd3|V@lq6}+ƢxqƉd.rb>4$LgmU@@US(p7VvU{>K`p;Q8dx|NGn50/ nf/M (SIͼG"_{a‰ݜ_m/1p$_و@페]2*knN|HY $W W;q({i@Uliy3@&|tPۜטjpuKBR(G}QFㅵcRzNw#; P:^. |=x}".cf8Of/۫ O}/ 0h2AK񯩍{_#]ouN}Bʡ*~c]aTeZQh"qU2%Cuic(tRS_GBE1W;*IG*`+irb2_Ra 4)X4ٛfOt7KlXĭ\4*-횜ܫx~OIT;)J[@ άL^jrDhxF0?t -g"|.y=${i:K "[;.ᶨ$F ƀoS6!!M݁`Ko‘oʘ,<ts9Y@ikC>nH+( @瓔;@苣eyES*BeXmcV_A;mg15B|.*y'VbU̫'@4j _U|r+)@rtIxcvΦEcEqUʼn ˕<.8 H# 3qYbDE]lw| U{3>d1iY^ #=*k>odf!͒LpaBi'5s V_A>ޒ/HwK"\UtNT i}t'H$) Eݗ)`ݰOKί | 蟸}g Hk3v8uA( JwD_1ݐ3kf7N} A%ؕ>U<˙Oښ0-~ EF~DN_s1E-1jЈ[i,P`H_Q"1m i;Ie1s&;dLfs$4 ~}U{Tp|YfTiѐ䃨KI Ω2&8}{-Q2*HzHuGN @ ^:nEJ*edH\N3JqXXm-Cx1Wɋ znW {U1Y#03MX a m2$P"EMIڒxXQp@Qנsd y"鯟r ?9 !T(5۔am]!e>I|'I WIZ)/BwVQgxI.TwρKx;]5IjgdZ9 Fqޅ%-a*`a} aEYM xvKRAo!Ý6>;HR>#:;t&*X,[܋;]56v"ǻU^:EYߴ牀3{\Y`X$;!#\sjF ]4V:WeT@} _$Y< 'y2Ih9? tGx.|1,oswXEPdmcvn p`HQ4j˧Eəg <PkEUW^~eFu>[&v>&uZG%!`Ȟ#.SKav|fgyhIY`' !i(CeZEC C'8xJ  Ś>5H$Fz=1XN+>XTL#p#]BFUklx/KbqJe rMy UWiCK>y 6jhJ|7"ȵ15GRѶUsةLyX1)! C4YnEhYE0G- ;P\NDetn (!oZ:rQh8LA&6:ʟP=} M<㍄ (;›!R= h O~]@,Sԭit/}^U]:mt1'-#hNO0ǵuK|w{*2TKOP˙$(|Wg7GO~qdC ts u5i{ px+jnil|G/>;d,!ȖaD5t%t zf2׽^/9L8´~?ˬ¿7V) ,At?P^'s|ٽ]'>)_ImX7}2brfbk/K~վ|oknvhSrDWI @uC,\SVZ7P<OEIΘF"6{X389v5/@ÝWW?/H[&$zwɎl>.ҹ4 }Ġ2T/mI"3hUb"y~ZI0u9~FOYu=h]:vt[r3. ůw#{$> Ss!D2`ˢx}Еa_%ߝ3 jԒf<ͰWܓ!FS?O[>).Tu&O4 Of +`[f&1CbE+8C ~h/`@6 ]g]9U1f^n;@kTRmڒ  mHOt`iRhj{e3:~f!H-ZbSd) Lq]N-`?m>`@vYQce@u]SSb>#;6hss[7+T$y{V +Kh6PEWׯ%gVdmB @ة-#'l :a\w4 >FOC^CWm,cўԚ!x/lD.p+@ L:Jd!aM"L33= hdGnjY\S^/j&Uњ]fPg9[=c;h"5w*&yxmT]re[jC n8W~ Իb:A\9'lc /U;.}3%pK볡"|p|"yy&'nF;Kvy]eRmLyiŢإpCJLl>yh U`pܧza.>ām|LH -wOM<!*kJ-eEI {X͛6dt Bk+9| >GX,EpvCuP=XOeY"=xwbxIqal&f>E<Ĺy!.X}7J2A=۫;}>StxG-l*NiRIߘmzVT 2Op6ܘ`m\U0:>3}FG65;t"77 rkM0߫[zō/Diy=Vj|RXWh7u]Wm+s6,zI>1z} l +Eꋈq9R1&@LCW4 - Bgk20KCl~)5\ &mOR3/_GrZ w.3KF&g+ܯ*a>Mw%L/1λ}.O֓umMrǏ!R(ܽ Iw` G镾Q ɫ9Чp*~OX¥`y=h -v<3 "t٫-^aԡ%a(, y8l2 F/~MPvar|bLEpF3 .OyӃc̯#k"gܝ?TiP#-$~ƥs T/6ߤ r d[-hG{Q3LR{ou+蚏M/Il̂WBXekʯH) jSM~>z!bX)p;h!g[+G_>?Ȑ lZU<Pk@;XtrZOG+L 2`T@=m?uzM{"Bx#cH3_f5L o Ae4uNI= tX{ZTw ;`4n8ڎCqc84RSj=YIUK>5hUXsdÒK*_*JR/kSz#t7h83 t;3BCukjaCKKx. ֽ]E`y/uWI#LZf:)cs)'AMZrxy6]f+Jo7\.R1- km)X|ꆔ] Qה7fN9_^\Ҟ[tMn +Op68Pz &4GW&|Bk/ivf{L5;wO.)w }ʾl,K&u #\ &V>[Z(W9ӚNp> E WHqtV(rB"(~0g$U.Su` DuacsqlQAA~'أKXqNJ=Bw_p2̫i/N)@+,$a1sl _`uRU+, D?-·Wp/݊#c")mN5s#'Ğٮ}:r{HssBJtm`;Ծ]i٬fS@jV"@4Z֌#QnMVD– @W#ӡ.kvҔ]£1m[2śu=ug}XfósɥZЍ,w/QG,hXm;ӧֈ@1hODngFRV=k2;O ́($đKD |}*dQb?P :[Ӳ96b,d/Ժhto!:N߉H` /n)}I [Ċ)Շ AQ+3X}dR{ zkq>Rh JnpKbR(BLeKГ\BMD2^,"9|kӂ43_y^SqJe '߹v(9%4:qQ?#EUa5=y e[\'N`cD7 e ihճOa W}J`YY{*LC2x1Vx璮`'񲷉SYPQ !+x`mQȔәi6BhOoJY+ S挅ߑ%t" 1M4glBysbiRںb/YŸ`s^ +C5C+eZt% UR[ vNLx))Cȿz5TafT0-;A`9w:>ҸDNX'Mɭ g-?U(Qe}Cpx!yq"O>kB>=B7ER͞2`*X7j e SF0thGFm/TQpz0"ZVS$v/6c[}nBVT;ZϮbQy"gNr]35"i=]e+&jTUHpRDR-jnn0q2_„}Y1оtϜ%ܶVy`tob>]ht4/u_̠ˬv&F^bD&p6[E+! i񄁅ekdS4T֠m 7|oFU#ҽ~_ Wa?4jXJ[H$wu5 !t # r[ELlLJDbAjߨHI7{X.#yHN[hËjj0-l7Q׾f01-&*wܑp2:yX!4c_AYu uJMx_o쾄kdޝTou6IyH7>GIbJ훵:+'se*YN$ީTv3p ]񍾤ȕ7LTQU/]E4u_˽ ,GhϨ[u:D!l '1 Y@dt)J|P@Q(Ex'ʂ?%}5vDDc Ҷ Pz fMM,--Jтׇ͸ 83\Om= z{FܯNvd"p+|>Z¼Λd~}_A=!RͿ-KA 7>uɦEMZeQpLێ@1e)cw/[Si9|C1/YD0} _&^Kԍ)eʈ~'Lz8= / , ;\@"La_$pS0|z n]6U,ݰq z#up!*5,rdSxͻصaj@3[ี>sÑ~܇=cX3Kk&@!2rb캩T\?IVU2] Ƃ.'Y]%O3?q:HT!fIЊ:Jhf !уXi;7Hq% ѷHQ`9sR\[MQ3& _2WTciA&Fr PrfNɴO^X́R2>|ĩfp<z}z&6I`_<eJy>&KzԐys/\s78dM5[.%>J[>&JNP->oEL [^zWދ10VqxaK&Ƣ%LT&#&> Anj{v\~̖{GEҤo?Lf "}.V ߬hN$bG؃7eck۪30ά8jSbǬebڗ"p 3b4?TyXC!2$O`PG]cF((veiћ+d/o~3ɵOc5ad-S/K-4r=Q5-Qr3 #XF0hgc 场ERMX=j<ًHM܌'DaH)|I\p%C(|F7 jtnti8;Kv-y-$[V5@y`QQk)4B眏2Zg9/P֝os;S&{>:lg#M]/F"* zD(G4CE'̌1W$$~wX M^FFe1ӻZNM-B.%I@oN*TSYIlj;Њf21m*#V䛼4àڙעǕTARU7uj`'hE>Kl+y=}6E{5dĢu;3<&/+uB[^\ s;zcbn ^C]uUI[\f;uR֑mrϬiJg|N\{R(h0Q"΃8aIXtp:&v¹3U0h&^''6md/jlܯ$foIeXl3Җq~ā'x@z9zPDn\BDU+ 5<W1&' %Rf`سVˋ`t" 빡D8G]N;۲Xi/P>e5 %8^ 8ǿ +Rr"4/GI,u E`玩[QȡDNo>^-g8wMpA2);1&#Й~֖(W[{.j,LO I3sS`ubxWDKUr7%U0$#V'j$̀R˭*#rJ$0NO.iG]aoT9gN:wv~x =5 nf%?A(8_cbA HySW9x6m/BWk̠Iq4ZJ΢fe]G)sqȧ 3'bܐdI[Mhz5ҐWV$˂QaT~G#jqRgY+϶G1+|25^F}Y d"SԘ6G$)Tqi$+WKpžO:`(K1V.1H>yTS-' 5E;+ L`<Q;v x0e;-.Rsn5pki*DզM., +@Zرν 5R0. L[^Սǣmi< O27 8\ͯlc=.V&FHFZ}}ĞAu56$%_/E_MXzz&]N z7~@fJJ$:lBlpT͔ܦ}3a2J;no#a41X'؅0N GUG>~aYN0m:y&̍5)\R*,BDQf߿Ԟ$^-:ޟ0b2 }&O,ާ!~!ޫ+b cX1r* ᴟ~~<MÀN#6!,_2q1"κǃ[ͅZ!\h8AG; zz?599?a8ʢN-2;+vQ@x+QVUo6aR H-Y:с3AaTEu8@+0H$\:'o1KYIB}Jl7U, єO=a`oөfIg*Q4,lVhDrNDlRlVM T ۟>󟌙]cVco3b.]}agsQcK2L횀#&$鄕ϟ-MH-UӐS]µd ;̀ǭ4dc8N#UEϘ?Q*[%:E fލIתeh$U#~A 2檌uM̠P<\ws+bSʆVrp|]#͓FZ ?eS }ϫR{.zhR`ɯVTl-7XZli `56s0H1F!o ' jA*8w:QyHM|a-A%1SOXѦw?Fõp ԮZ%ϿŞ;6%nGX=!#:X&VUH^yBd,^>i _dd7&DWNk)ΆC_tvKDIۂ=E7L le$V =-A?΍Sa? D?J>,1rU0bʘ{bfHd$?|7=_o\ w>k'1A㴺KXwf9I$+h~%xquFu٭HN4S~z5VE '\fWO<PQvJ%>f+rޏYrP5!Tɘ잔!8D 7bQQxح[äk;bg㦍taYb6jAP4 5-f,pB 7]?y^6.NV\]_>S氻v?\_V4rඣ)wG_]}TUbvdk|ҌRGAV 4&9$-Po;;0ט覰tGE{?؃>w3.O 7:b,h_A^m5 |⳧aZ{2 [Z0C_dB~bjN&W ]@zdz/ I3dz(/4b])ny5q%GI4GK*"fAT8 H/(s堞EA\m>%pdA>,=W$zէ2ƥ5ym1gxn>s_J gӕ)]j@Xx E9G9Rw :ޗ4F=(0'9`XUM㴀rXKw@ QfC q [izA]vGw:R CurT窧MH_ hf .zۛQX^To$d &65`wsKR g]bLlwS$* r"$Wu- J)=ė J&һ-IeRĄanveev |?ǂ-P'Yw2{(r$ Cz8##U9+G lS܎n+\ܿł]b$Oe}&׾)#',zLQ-3(p@o]Fu'%KiENh5,'DH g-B ï\5H6r$%ɇ, 'DC6m&BS7 KĚ(aXqB6 : dj hwFwwgvtz< %cl$#^Q #Gk7a$d!&~?kj:զ!:W.P ȅ1ce0ir{rʣ.bV DCiL22&N2՟:yDl86SU~+<52ahͬp<-7hp 6y*pѴxؼ7 @6- Aq]3\Tx_Ȱjeo x#ߣ߈tow{B[9 }!ZW`ˈ M>[*YzGHy :8%"Ǹew-/2sU&|KM((w0}BOGK lvO㰖+tƑ`>O2Q?c~?- RvMqE\|hڲvzuwGB5Jܷ,?\@~ +"T{mTVU\rԎs&p/WG+n-E(&VLΩ&ۆ7M8E ?\ VFIV-M-°V T8sysH r1:u4$VRަ4/rmal8Ht Ꜯ3K\>=mp+pAPl/<΀{z2 3Z2 u٪LFSFT)gz9S}NH.M66A>:{ʻctUg(25}!1 ўB(`|I,g$,r5Af?zJYvךrSi2qr)v*QX~\u&fE.!yWqZl:Pc.bu1lӄ8Gި*J $(˚ؙWicsgnW:doĞsc>| &#v-(M^Ia!n R9cq`jm<W6 Z< G8 O茤+Yc~EBec+HKSv [iYM1zQs0~*SFNm8= z?e^g3IV [r.q0.;qy-<\D1N/w _H1́<Fӝ*B?"{_R tŀhgdF}GF /F:EHUoD4jI^(U@'dlnh'W摐툮A;nAR! ,nޔZ:KJc\1f?zTW:!8T|"~gCebj__2;!0򡊑+۰cZ6>pi[YLQ kLqp{!G\CSؚCn \|xO F\*yy_SY*%q3ﭳՅD rFd1Mc8XgXhMv`cKRPʸ/üVk?4# H?juNpAv*)i+SB(f[kqac:DӦe.4pPo˰T-mL\{Ys!CJ_ScďfL zz?T;Q9@Em YBPN풐65Zj Lo;AJ -~tp=.T?U"G.E 8GdMI?uRHnZ7!EC,/ǣ5?|@ގ}LhԝMNK4cPznYe d*I٬>a{2Oc}Il5C$X&5j9p _aO3 Z/Q\34'&KxR.FBv{ $ч?fL F-ۜFܯ S zƂJ~|O[{^`pp3)2t+/G6!! VsUL`ŧL.7n컻)4PRlKcLV~BZ1Lo#ES,ChbLTj՝z1 vW}\P2p5 0ڭ,Qأb~yȿw)Ys] s Ws`cM [|3b  5=H&хF&9 d@ nr\oGX@wBI &ҟ'n0_Sl&Hd'ݣbShDMrq^i4p^Rir[Z 63]Wt׹}>n1  (gH5@/+kK^a[`CʷSRW6SI׶0В:OJiB=2%5R eYUaꅑb,A-mHڡ;ĺ{f=̢F:R-^8j+f3F,/]?~7@ ? Y/t `˯dEzTuO'Cq {0;@4!tꩋ,2MJ%$_? "}*f12,k$@0e)HdfSJ#рM)SjY}4,KV. ]LtykYpAKԚ̠YL|TH@@G3O 6d!~wej#`Y$./z8MUEѦa'e$la30h/105AT=Ëxܗ3ߌ&DwMag2Bʏ""*ՇfI:KYs&9zqnm#ɉ4"zULﺻU^\,C23oOxЕՃ@o*CW+mW~LЭZa$/F^第X@q6Eٕhw5 r^MwM~aw(1QC}#exNy OU-)ܻҀD" dR {?֪,Mitđ9&XWyLU_m\x>jҠۺFEY0ʝBPX{~ V}** 7pA`MHͦ˚ )x:VZ|JƇ^hbZ4~5&jĘ{J÷TC§7G<8o W.m7/?Cegdroq! %Gmr: ~1Yj 4Hކ==%%ڵg@05eJ ٿIFB@y70Kn{S{"=! ;C(rA4iv5_$VhhT24B&] ^\qFEGΣ&}Ll|<}'p/,ڠi/E|JUܠ vgDip?;b)k5湟*N#|O$Fv'Vi$[:lufKrgs %QeK3<Rfp2[}3ȸ̵(Қ`/ݞ=8nfQv݌ ͏^y]xhD-io_Ԋw4v< k**:iO rqc&420 iL/~rU_dx-dI"t.,`_GxM~R @(wꏔ(4sx^7>GKu\^JR\u$LO{3[,>ly/&I^KnRJPGUK57}gntqTrqԆ돐L/_K쉧_3=mxOcWN/`I\Eލ Y'EϘ &΋Tu)& 懻&N@mnkWÆ+Ȅlm7e;xҀ,EB xWm+lhdC=ʦGk4PlV"Seh}C!G{+"qa}y}\4ě}-  ~_ Jϭj>O_MPFCUUCf?0xCQDGXx. Od"4'!Y YÙ#`!gvd3Oh]('c@lf |'nk3"Ǔ`kc3R!lc$' jɲ&7mbٸ?"cČzG)}"*\E~cvbլ 2`e/l/Qv~!<Ն=Y16$62oj5RQ+~^+Ln:97QOS˜ɪ)גԆ&y3ݑXd۷%RKHY%[toz7npRj%װn32NгgVjMV|Q:Z*7<*)b9g5@YaT`ߟ;JcD vwت=[ְeMc^_&fc"Y\X"25yOHna1pe묺JZm5ӝbsO0ܼqPľ]Dw?J<"*@VftȉlDqY4l^(-"Ԃ syRweGeu4%mY\Wxo](t$iN`i[ZeHFvAQtwvRb^waf_ jǪ',Gpwq>V1INPD-'7T|Wq8>ٷ˛L{b+U1,aA^!sodCT깦ܿX_X{mT!fv?pƵ库$ ENG-VW=vXŀ\qt2%$lx _>Oy3uNÍJ# Qx[VL|n8<ùX/VmAnң6B^֢C{3ҁp(7KV{7ҺjG*r0 w䗢A~i]Qo3DQҎQu[BKHFL2% ٰI۹~t.$oFU/bc!'< 4J7 dP}TU 7NlCrdm:]G5FW"2-@$ޒS>y`ڑzY~ >4)%fbl0MGTݤү4tӪ/LF# Tim0ֵeKhf{q' oQpYv*+:?43HFz[Ƭ S|2{KFDkBzލHI^:M :q_[)}˞b\QApp2zsO \tq8yаq0V!KK4jx'a{Q_4N}اw:~g$FVXdTtl 5 maŁApX i;]+!-UU._R,%/|-O{PMY[4%;Oډr,怱L8@]MA%PK{ٛ Azk.THY7_. WJ#X2 ڥ6:33'~6h(45 5Ag q#svHo5JN+/-l;=jA[/H\_]^6d e5Vdj3'"W6R{EXZn0)E ikRw5%-G\(ԛ"9$JlLz"6K l] 0E*XF޺*"x'1d;Fk>ђ j~g2@+DS`d`22(~̝!߯LgkÇܦIh0H]hv?Wo:'XSz"U0`^&^A".?bl #ϵJVFQiHV&% T{M4rzg'Ti<üA:)$,x'N+TiPmEs)˴CenK q5KO~1p+P^׺r8tH)8 (X:u JQMr[KPBFiXKh[;LE6+K0Z<ezځٜZt6^a M|򀆋C<DckkP:˥~5+6)H9|ˆ%%xŇ$roJ- :FgM̖z#;eUC3[pm{ie^\xsVz*.z´wv0:AF֒,Y>kb;σ=Ǝ7 ;vw.Pu'6?J ז[Ho(dbYK%l(XQ5LQOiP1 S&ýߥ{m@^%$]L N}0|4v!C gs5ׂz..)_d-(pT'6]w,esp?_1M 84~saMQFkR߈.OM{bYXu62btF׵@vq&etw]P^p'S+}R$[4>Pd9%Z]9;K zǙl4hmKEH84s=(~UIQ>Y搗b5W~0&aK>$՛xB]b~u\<n&hrv. x&z"OGk!.p|\qb׹'Caky߄?I^]0`@8@iIvhI_M;CHp32jmiрbi?)eĻgVUĭ|ETm#:peJVe*EQaTn,.Qh1}?G]cP F,Ry; ?Pv_a/2BTefH`}nytM2 Mimu,f@<ÎI\Ŵ!*+"ts+ ! /;pE)}}-A'lTRx D=`clq]jOigۚAv"Mcp$AZP?ӨI]T.3b]87y_6 P/M¯GιP_ywȊ9O-6zMl5"AC |΋<hl؜Rx/ R%zDעeF!`|>xh- SdXyM lv߽ 6FcyO{' JꐤS"?ɟsMt2̪)br7eKrs_yY˙_UeS~b4'.gPSo+\2L0+J/),Y_sEȖ393#yl3V x%WeT!P|n/Ž\b* h 1jz{=Cʴjg<`!76<چi3-cޠu3dY,VE)hgbz0)&Al%Q7ya0d\-_~ >PQQPz5ZGčFqBk7\OF&}K5ےG).}hpXBO^7D[w|VH`e`bc=V9,Rpј`vT4+oq@.K!A1r>}aѼ#얾z 02Ϣ u0J2%)(e$'α"V]#rjːߦ)>?ҠFBX^^jWYr'TˏJ'~V@w~U]P)H ŝl0E%Vqn Y:j)x\3v4DIUiτq8.9"SpVF9HMĴ6jVV^:`XFpaf,I9nF1[ǜA$UL ?s)^:q5bC}Gk EBs9wDM$bkUhscm-t`ێljnA/M7gƭM٣(Rb_# Gp*V*N,`WWNFw_)h4Ye_k@ 13r,:z_/fy)֕PǑkOO?*M%i%< ,UOWuh.xOGIpm%K^NEn*L$k#>8E#]LӃ1#qh ո?WV!t*%u?S[`,3zI1_b{/MzY/0wgQi/n[4?6<惇m5c%b&ٱgmCoV{-Cv(_|kykk _k ϰVaCçQS]sP{w+k;k8!7L0BO^ %z 0@d=$S涨 ³3O# \K`YI߀>n//ne!}w:@5VXĔ^&]T}m( GKW]gmѹ]T>9sj $'|Nz1Ñ:تJÞ0LHp5{} wƂ,7k 7L`օ3[%铟͘]qnmTswAD{,4== G^[f >rκoa{J dwk6ꎒ蔥,Z0w%.?6s&޴>Td„5&iw|奞N?Awyfxm.?8 W5}l#zA'z$I {P#Yu֥ s{*tܮt; ޴r~qN-A5W&Uwo/7  k{E, LDAp:%9^h_ J~,uпF~ꤺ"LuR&剔U3F{zk7^s}3EňgϵO`#h+yW .2F%Mm"}.!ZeNV/ ބkHWRS-{@'KF#j4ӟ ٗ |+jm&*KL2p\0NaVp$=Q5rnT\͗{8UE2j_&: QWu .)-Pp^luſ!AkzV@ggAʻqjcczړ㩖@h w^a-0j\D:aYSDߞDIWMFI$(nK/=!`T״@OPM&ʩW<&e'B4 Ҕg;H)L^'9hA׆4F(Ek!tE}ѵMz3aUbz,+qUP>j6Жʜsj!$fc3X+`5#\٩1 gg3cu Yqh-f;? >i!^̄߀xx]6)XL*ZQz>k `B(6m/0hM(ʮ֐d#'1^H k| [Xe<,R]aF5pO-CqIc5J*NN?=dAӕ5aqI˝tCk{GzB\ oL1.1zV@?!`9eg7h[kvC0)al?UN0K ˊ"W{f`ٲ:oȅߌkV.wx]*;U{y% Aܵセ%$!?= fcP*'T@)!]~p$)bX>@5j"oΫF phN`$5y_`%CI K/".żF:V "O)+rvFr<T=d3c{X\CC|UIp=^;z-gXL$"MϿ݌Ņ@!)*-^oݫ>ֈl~z y012 mw}!~$O0>ġMDX.zKc[D%ލ\!ťջW[~'5ZDuKJ[Or%>8-(M$9z7eqU2I};<֍)~nlDw6 ?!;|_L MSnYDcΥ_:τHàMil˃i׮۫]aV_w I8lyDif sN{+Œya)"9;d=,=|k/ٮ0O1Bdy d)Ҹf<ە@ _T]ϑ=`vJ~P߇Xl? ڗz^s υ٭yJVSIgx[<ѣ/o؉ Y4e9+S>e Ւ] "r\J7'm8=R;Qh!5G1hAx&.;aCN 0v<'u Q`N$I% UR.lpi hY}@=RǙ%dtlNb8޽s3t]=^58 Rf)4TWdպctocI896lY6OJn P}_}dn?a\OzNL>pr7b7'w3՞-~LBl,x JT1"ZnS%H˶6):Ks&ǁH\4H(srRQiPP~` ? :) AY], wR먺TOuCe8@tn4dQ:|\O ~9 H>˺h$,z;~6A ֵ { $RѹV槼u-N"pf%={H4>(]GԜj5Ieм$ ̯}UX~ [IX!M}!/xy C _+ewҵ3 ֮pޔ *ߓY ܏GNB8vpN`odqsx(e  ZiQYl~2&gE\_Q %^%Jj>&?!?ݶ%=&, OxxZ`0YcJ|tMCC9lFhN4V[Iho: .ʢ+ɮ r%J+98mhH]bVYR< btӻ]A~`Oյ F|L4#MU?R+X͟: = R)ݣ3a;7g;2H1lu';{~jnO7YJMP$ OڶJk[k2 h7*| ʖ4t]*_ϤǶѐFrF\PĒ%yzEͯ IgEi C@ҜXco' 056?yxʨ-O)0Uy9C#x1H(oq -_Qp'Ѯ TKFap&)R'C'6r_H9"ǰ㤺ˤ+٥(u*JF%>ɗԯq[͸a;I=gBvPD q)^&E&Mv+o:-koW* @-\V;N Փ{6&qD؜oIjwеۖ G~^ԩ߫7P1( _:/s6 <y;xPPf;1pb%( y YizxuCm\t RdF47_ɂ:Is˴GKքgӍP\Oj~۹=-Ku*RKJvzOTPWW`^T|uFճ:}wP#K`ʂ]\y*[k#8wO7D8m~`H?lp}׍~϶[J*ˬMb(~l﹔2ԣ_ռ}s(+,ǒ0[E?_/ NLqEj}k4Dzb8DEGbܣ]8,]qK!F,!>Z`*U R>NQx(!YalЁ?3SMߔW>.CȉmNsgSϜr$mOTyy)ƼZ-fXMiLۊo)+[ߪۨcBfvck!oֶ#tvsĥ?Sޖ=Ý*M{edl"#Kt`ZK|2_{ \ A;T'n~c.2SӁQk5 Pvff{637_0Tۤ{Aڤ(J}7֑p?l*I~2N5(Y}f ǻ*jg_KsO|%>&KZTqn6si9ӵr;\z{[|mirLIPYQ`)R~>͛ /0/=`+|TlsM a_~lʵ: )rj}:GlC$5b"p9J3mnj?sE jd p0,e劣Ig-h9=34XG` f'+m4mv2iИ8gȘJaE)eP 篞6xtvxꠓ`6t)e-cά@uHB6.h<2=H;7;/NDG~"`m!͋ oX}=Ë :Ji!}ڢ4}k(67.\1ODfhغfa{'<>\m1IӜ[1ÒRxDlYSyYhv a.vE(;֨T6n-fg/5RDeO gߘW)utl\MeLzOS>9i\j.{.5DNt˥[N~Nɒ=q΁ ;r= ҅IT"}3KR,+ nj\Sx'X@9]Qk\kʼn]~ɪINT()gmw86mH[=0eVً8֛Ѝ*Ƌh۠sH4N5d|ʜTc*qǼ[Zn˃+nږlǝX>I]nj.@&SqD}aT씎E+H-qG:7:Ow}=> j Sx>.cQI̸F:KT^S]ġ.֒X1j1lL궙|&I.˴,g|#Bg>sJ*p,uДY0Я^#aYg0~1,mF}n#k*Avwq4 ȿq1bAP!KD J)Z, ͊x'탵Wy~,lsRNb{^ ZzBXphWYJ4Nq@ sM~r]D?Dg`Ux(B@̙\ 2܏53mB #Y!Aw/JDGFCDh{rUt2$7p eŲ1h bEmǕGSe&ҁB`㷻!5&If~4@CQX\j趆+G8p(%R&q+R=Ϧuq<ԣZ2sBh{"b5\ qW\dRκjE{vPoLRxq,;t\–jo4%Jr3SLĝ&ҭl>&t 0Fki.H˱AaMVטw6trQt &xico|lIJ:S(UnScG_[Ӛv$m]Bgzt ,-&GAl+4 LY ~&>;_I`$,7@ Z%<V^u'K!<<"m=RINjx<:%b3z{R7XQ"# rGxQ8Oͦ y9lNM&{f3HFH7ǨTT7L,`TX2qbܸnb@ƍ0ZwHV5kϊWܞM֨Ry1:$1/ɴr}lۿd>gu#\3iTm5YjW UJhLQ[G.E{Z_Sѹ2mFl1Lk6psj~RixJ#|g:KTZ1CkT d *XgE==澚q^W"C L0Y\@ۜ0]K Q-%/j!y^YҤx!&s{RHw6y㭉Elcn`s~YEjJ¡y뒞3dkwN.S5y>[o_w-6% y{:6a,)"Zh_6[tL? #󃽥 y>Jc{#LR Z]lmY7u=XXUnfrLȲ=3zkE|^׃M %lDkm ؓxk|3X;r h՜Z;rH85 -l$^Y T &zƨ.;{O8DXL/m0vv/F?Us{J<ۏ@$9}lsӞ˕.rNx$*;KOoޯՊ@@/S޷/!2ZͲ& M(~fvƞ4Ș7 )eEblJlKLBu-Mpv@aDcK17"bqXhW׬)HA=ǂ7;o0uBsgED (#~7GH3ٯ)󿶇x8+;pāY9`2 SQq;>6T.W<2uDvu X> xҵρH;UM::>[Ge YjS9 o?N[pl糆Rm(5wXt--+TSI|78|\?@5X~jAs^8\]4KĵI!ƾ1ʲR;}U}Jh{\oL &ܟx$9x$" w]mס"B:w8oIx9cR{l~_C==P(ҥsw՚07xL=.E?W)-yMJґMSd}X1mZLa 3d1B#$4Y36e0LObڕN\k#/Lir:?|q?)5E$ؗfZMܝ06ǞZ_Шm/x rDbSAlY\ /'^璫9&]ZYg~k}K'TGe޹|PpmVI0: y-g˥ DF$=s \;QB 5(orŏBjZ#CovZhG |C)$,nqjYtCSyާ€i]}\`d/gEܫ~;qҌMD:|^T R4IUwZ 4':VtnyaMK1H}TV!yÑũO7҇71XCl3ˉd%5Ƃv;_#KjuԢ&S@ ۏ^=ͯH2 c8)" ^=WW=ܫj8"`oK?^1>\uS#xeݭAC!Ȗ)yJH #7L(ZC FafW*5[[l+VDzC:6>q g[OZezQR Pp&GcrLϻϸa3!µ))N-k#%H`1tnH ,Ezۭ*~fL|KG)ec;6 lq0^aQ~qZRDۓY\]d@7E(Lw}, }ڠa k]ÕԴ/T&3"zMC Dkk̞2:~مs6/#?uzEl8ȆX=jIj;Y&PTL} pfyZ TpV 7B Fbs;ǿ;bSB8^dH4XPB %-IFBDyy=ecĒ&Uݦp}\m\".^Ԁ_nc_+|fc)?llwe9- )GBܩs8Y`%sW;zITui{iD1v,Em)bF79)U[#7Ӗauu$Q<[q.)}WBqXl0wg=]0n"3T8UVhS(_󫲺 9&%/@XJ6!fr7.|v {Q! Nο,qfQ{#qTj.vRxB؜l6|gwBьJRB} R0!$к0g*56\,+QkN1&K̯I~lw $ܧޏt=v$2v ABٶ $oCLC*o3{E Kq]:#VYV$8QlHwVRU^Z]jo*7kIP} Ծk͑={MMK1]>3r_$$@]N@*bc2!|Wo]|gjSԇc\]FnYm(2Xk3+u3.wM"XFoK($ ߲P2 uĥ~ʱŹ.4ѵ!c, ݉X|Nh0ĽŘg'9!R~*A֨ Gm!3Ԙ6Bz JS|V# 2|n0Վ`(t@;:U-lc&”8@Gl4b `OhN4sh͆S7;E>-Kgo߾WsWu4ҤoV/Ih3r*ޝ4h$Hra h=n$&gc6EXJ8œ4Shœ09VU`RwdvG^ƭEVMk?>Bf; ͺ|yBK ?|>w;?oTBCFpx^n?%DYI=p*^-04F8 :o ?$v(qD95yY}0muRş4%!$SʛCGIIOj}z+t΂L/Ig\=Z5[fv\>քw dɠAS`&5<ޘz1K Qۋ:IҨddw[L$$ Wf_[|xgҁeqd1j͖k= ܙRmjpHpF͵0hܶ@;L2)@OKz6)!^ M!ci[^PS?-]1{..&_j ?P=*&j.Ä%kdb s\P8pw-Յkzy_Lm^ytĶ5⩜{.PC{t> +чzH3[ʡ7K aE(s03mx x >aző1of[HvXw6ᘼJTVhZJ0(JGf[ElBppF@6J4CCcJ`NM&!A,si@^͡N ~zZW0OM@5?ot|K:n'lȊ݋;bOuH21J`&wi1TKgEz' )}VHKĜm*1R4#w$tssV9Nr^"`jq7tOa^g~!s_@p܄P 1_j+rCF%r 2 tP6H^=ig#uުvIbФ(g90"#y}lc/[H__ax1hAc~ꆢ:i)XVv~ՀeÇ=\.2ԏu@Mr\TO~C IK!/~S.AR B V +r #Ёu68"3 m[H&3em(j9 \ rg,~7r1EQi'29Q% 9^CVd s:9V&(6MkbAǐ6Sxy]_Y+閌=+}NI7ᕙȳ!U|H̔FM"rD9ftΨiPl6N(m MDZzjJv|>@wǍx艕( s,B*I>38͡u^qeP08{r ,Z*mzЕ+tfnHue.܉fR"\R4bdlisjjua36WRU l!(NT-T|/o7>TtV]8G :s.b7ny_>/no4yƮ ne)`}$n1$Qo-?eQTpC͟uu@~/!t\cIv =/ë`,^y:2LsHfE tW5rg"Ѹ_&j)#;UFQ'%LY>˙Cp#eݥ3X2=L{T~PAݷ% .Բf=Q;|z j5Y#k(/Ş] .3 L/oKAE 4{C(rzHC434A-iB jM wk42#C{Kn_(C cI Yygz5Q~~>]~ø[{6" ,e.g8Fĭ,ykG>jR_ ̩?4[ŷTX.񎝉ⅉg|d7A{E;:B[3d[I]akL6O4[cH~B QsF{|Xuk'Q )8bq8%9o #tjތ?*F%7ovN>i.k=AhGll5&K%:t cz0]W 1jZɭvi(&Ԅf"u kA߹{?I [_(>=$N/!{,'*3bϔHP+$ࠂJ03YTwH22F60EB*raIL_*6LQx zxQa}SAUT'"wIXlh^k?)@,4)ߋPB)$z3Ғ #yU[z\wX"}vc<=5c587-W͆jͬNx8bVDJ#d> q]]'%k`\66qe0>V p͇_D-@; []@KJ ndo~MF앐/c ^FanR> 72E0%2e6jo3pg!_Sm3:Й& T9=Iγ:f%G{\Y# Ȋ8?_#6I ;Hso[qXOrK8 fd7u:ٺӷs Z2tNfEbI򳆤@Q7o4)+ ָ-)"XϽxjT!΂3 8 m(cQ_B~mhۏDZpWn YF5ug]V,9bCM51i<* L_"ihNQx@[`c.,#NTR@`eӠN`_U5ty+MtqcBk!?zH7+o!s%ES۶LgV"M TefD騳+L#'C:i6(E|?mom6;H}*{H~WqqlKYXL!#Hd  JkB_7΁wr6>9 jFA+ZC?G_ SH@a4]~'Uua+tzFL*2DQ#ԎŒt>3bw05p`;0<Ul,pR5HYvn;忔*DM \ -fĸ!Pg-mk7ʭgDurjO.$a2܍SL 3;+~qUvל|,3PpM>ksei^=$>bdoa6+we^\P|:^ qXZEeK~❖aQ̖ǝ)e\~^A6lC95вMH;1g:0D/8Pn<[r^=Z7zR^M/V:yitII}0їFL3Yueጬ|BE2+nTw.hX@{O|0'"۲sf&x'MGs8|QYqiTU=Jv49ey*Dy[6a݈$Pȵ9g&ߐή6=aс{"2&&0n}9pMMZ/$ŵJ8p7Xb#ą}2{+S>_l\.{( [WEoϱ{+ H406K"rVE}MxYm2^@D[OosuYQWSXZCw ɜ{;{wHv[Wн>kq3Ϊ%2* U(vZKx8qcD3-@e/.ZrJ| ҝv,D&B{~jG,p掙[ZZOa$.,p&CĊ8|yrB'ɎUq^hbphbR5d-t;ڷG8?^+'{arM lHRs\.I8σ!?HLYB}-Y0U"'G^_R *;lՍ 64Rx E=N7\Q{ɳ# < "MCeؑ;d8 }40sOek~_S^S"z ܕdN \^"Gxb5e1?`3P ")y7467l2W5c1D.7͂$pD8%yZFJBliH&ʝY.HMqךG yfX5YM޵RM GX:,1uu éy}'97\JlWS6r$A?a_\$4XP~OS} ]_^'d'6ZˈXfuHݞgh&{-,9SzUC+t-y'CX"CKZO]BE /T@{O2Oҡ'{~|r\%yI%$"eE\Y"UWS"Ē#GY=B0@`9榭wn <'>81OԌ݆h?,x>㑾ۉ<>JPDwHEGI5G$&{P^I\UyWFWfoϾ-/x"Qpa3wMeQo b (1p}5i2`Aם˪k4k@\TM\?wmzqShbՅ W>Amd{^P7PiW'8c}$Mz[͗{DYklAҚK{ڕGh.aRm6Ӑ+ 6c($i,/EGpp}M#oXx 0BzٟRsr+Nqj0ϕx\06p.8uԔ.%3fA,ZK3~jb I/ejyc1qR>nmDRkޢjXMXk `Z(DJWfXM9B@fhz3AFLNFMMsܣ-.dz6$ȯN?D~XCXQ]owJ)A6#9_fXNNJV̗eg҅$'2]ZI HRlpw3/V}b(#lM9(pѤb=Υ- ,X\ޗ`_ыS>e ᧵ew!Ԧd;gn`%SKCR S[Yy.sԍ+bBc(RUFk~@B9`w}޾V N9U 3D Pi tjx\áh <%aGhT( O1֐q䭧]2{ ߩE']*g:7<8\[ 8SQ6n 1NHu]oOL(gpuQ +ſD;_k|21x8;!¯mnʺ>8bۀos4`bDU,Dl摾\9&{V({vƹտ?ssj#)Zi"F?\01/yR$|DI YtzPcxLCЃa}Jm}$s炝l pV-B: $Rjzc9SCJ(]2cb4yj$J. wN۷]u3fNp\z'64#{Ɖ헚t>œ[AuKmlRb kX<[ơݓ7ɦ,3" r+#Z v`U\ @rp|QbsIE ^{k~$ JEܖ ϖf3R1wU?#YvԆd6,U6L!MX;#dN<]f-nlm[ˇk3򄫗U!` S2 bڼ̀܈)A_yS :2|~V%QӲ)ID̄hg-1\6JOj?ٔh-Y /.;]J`ar]8t{;۵Gw/;SaUW¯HˢZFANYNWx1R*%+Aڴg6)c(xӱԞ|4fau8`DF!]Q5$m|gQ>etaNR:&`|W+I&WYK2 ]rfĝW\$TDuhmI#r!dDt~q|Z%ˊ@sn] a9ʖlII |Gx9-U½˝%M^>K GYy 5V9}MKfL<ʟ:P g[yO⧉0ZǶS$ˎךmҿ۝k别ϭ;P-<ڑX[hԎGy&K4(C=ydM&̒u(#RHEydb/Y'6]a.i۹Na9Yӑ+d&q_.bgSwP@|gLcz7r;'Y;?H\u}34GRʤ_|SO:3?pF -D&Ɋs1.gcCDުbz_~s? 5B!&_+>Re(*um16@[NwQBHf^62IdSЁ66v+LbkDmLp WNfccS g@K{6\ҜUD[~hX|⧂KkMJ \3"W4*!Cq0C@ʽ"2dZІrpS!j|-/V=Vl-ƞV|W 7ACg畂?\2h< Y ՗)լvTژIZ] _[ =͗x[겺FߤUHT`:P*y85)V&? u鉰/R'FsrJ$G/zL>0mASuECΘ<^[qqjz h{8Pt >탲MtԘ#1ܚXC$T6oiAHъ_㚓 aH ٧)Sr1h=­21Fhd/13 1ۣ=<`XV#du0R^)LM vT!eҹ^=8)%mTM{]yoLH--% N?W?$4%j'1. ]_T'}ZE D]#܉ト>6XM{#BzY9dJ+_MVKb@TqԽeDt:D;,n>@{KEi Jf}P* T;!.UyzҲbK4 `CD{+#[KFJ#Gv``ExM,+$A;n)á1?{Dř]/e2qL{n~DyjIi˕n#`<2tx< 4d!mM+&'W4.aD },<wk!8iiPx"wj0K݃QEץD|3\>Tnd; g Esv|To-e$`U]F ¥ZYl\dw5O!}MtȵB+h+l >WđS=%RϢQGjḐ>FJAI!f,<1oM,:_HdY[FvxĄpFfxPͬ`6 v§WfF* ,aя^@c%Mxg֙TA)S4{*XY<5 L)NrZdbcZvG5L|2%dP38xDokL*(F!<\Ke*rk{H9fGv錺<-n;AXc  ٸ?;A,i[jVxJX߳^gb8pf1BHq9"-<& b:1k{-بs#c܅ץV&a)=9E G\4۲4ԟvTIx a X[ X̗ 39P̺tkU*o: [㴧L7@:ReCs=!%މ~WY3^SvͶN4:zu-MD3SWm[I hF_0;93JTHJ~B+{3ׅo*KCN20*4}tFL|U2\-Dsp`]$݊ѿ]3 R/){U))ĂW9YRfﭶ{K omt Cو׋ɗj!SeiAB)|u/<H~5Z.lAy  _OX:-3QL*cm]&d|}z:QW"_RT]:7rCWuZN!5jrhX&qfpi9j ]7w`u)ں|CB݇@1pWä́!sw"& ZrPKZhLQP$ 5+alX^٠8P[Eh8J˰(ЋW{_O)}H8`\ !5ad9ΰs)ry&w"USγNָݤ|}l Iziȴ~ƃFHpݠr—ɦdҠ{UGQtõ.0{P ]=OǢ5燈Ez9숿mY~#|KŒ˚PXwU4bp,\B9Mk Siœ "1Teҹ5f8aN'e`#sla:gFXY> Lj#3Z!N=ޘfO8QN~<hMBqZ|r.8o; l=*R_ĽߋĹNUH%RLR4z pSU}Y'jKT[VqX 43c 0ǃ\h\V[ۿ8:"~aɘ9v)OGX>zU&栦ϔh\5xkn0ȳ̦ت 4:[B`Z\UC4w [Acu Rz[Hܮ+?pAPVHXU"hXGw>³jAC괨_dLI|b !I5QBӣ| 9{vfkUeW%;j 4J3»8yr`.2g#wЇ_Pk62[!LlPo[֫Hl&տ) e'>-=)6VXَ+ R5rźq k3DžBUM6`r{T!ֵ@UabJ?GyU!<gДpD[4i#w??b ]YV `y,tۣgȱͶT sW/Cx̀!(wap"1J77`݉tVp6@Hmg9FP}xV9z1b?DfA j$4eB+'uˮd=j"iz欳+1,79>?2S|7۟Ln8h > Ny9ڙɂ[eJ7>oҵ_~;_W\ @= 8hbj.Da0]@jZ Pns. 1$һU5 ^3DR (Y"Iz= M4yH*ֱf RGC-C8}fA4KttbC#^mI߄P,?;ՑQ?=3*UM@Cxe KĔލrStKTWy)fǖ{BY )yvp5U(8͕ane)Iu$qaޛȎ> ?<67h{0ܨ}jk)qWV(MPX Y9z(Q`zWqVK܁a83oi%+F( }Pϊ40 _|tdԌ6C3 Q^xZJnWӦp!sH0Q #bb$c𚇶2ZǕ"#U'ꏣK"(0\ڮg4Ĕ?N2N,7 r>vA C3P'+eSh@VGƮEoR[F Pׁ5Cr?=>ErxC@\OeT>w 4Zhoo“At2YOY >1g]F #7x@P)-Wh%J ځ3[N9t7%kkA%[e5.XDIp]{aw9uZH;9Zi"i˻ Li%#b'f{q(QXfnG`/nh{ӟ$1+ըs9?VWHN~XSβ/J }l@w %֩tTV:B-uBbsƀAnFxŕh&aRB>XNGT`87h5f)%_ V٧ cŜU8~oR,#KEξ)b:1bԋJ^Q8 xKNeD  gGߔٸ#,/FR b(~؊!F}ۮ]{emcV Y15nqeSW:.##r)͞{{$ڔ律52 TA)JBu6w:y)g2eQ & 3 {hdy4qʭP,Ж FYm:HoY$b5#}iS0ZܮU񧇼0`9u&7"j%''IGa 5"}0*^rGAichuz|t[k=5Xeǎ0S46n7~DL&F0+COK;O*. k(5agH@&>jR%XVt_ P^}A {@3T1F~v_2H6tx=IӜuY˜x #JZa|AI\u} zh(-hkTDUNߝ,'ZH蝝9x\%ujD-虰uz%w}@4GުȐ)Y?̤Q)> vx^Mxh&:pEmu4ukFxw#[ ]1B{B(&[1Z"9540xllJz s.` ~wc%tXn=Tw6Q\Z;,LUʹ` H%>E;0x<[̈́Ah5}~ZRkQ݃f)7tEz5쟡ʪ)-lĕ>Kض 7+S#TQSkg[BLo}I*{wKjbhS*O&.#qy@.Z|&C _%R y{% دGT0y"lM`Z:ɋR^8h{lB 2oU!~-R՛6LKh"pǴU'(gb ?^ZzC~ߩ+I2JK/ (`"HS"ߵ2*GoTeFH|7[@|PnBz/1!9kKmH/Gf $!*%vEQQؽvqفsP?{pIu$݃άc+Q45&zPRXg*#Y..ǭt'QT~2%їN,c{d.eNoF/;MyyLI7v;$2LR%i@fbߍyG0< &eg14!?7H #d0F<|6j^ǫNL$pohP;|_(rED[|u-BC>aqGf?*:p Uܝ2[Q8} `L 1;ru8K {YG!B[Jέ 0|E-[Tw8 ~ф_z瘙yW#Zfpi93SYZ- fL|s,Au)rJдA5$)6t>s|jHL쑅~*Ljx )rxA.2(+L a+m+ZL7We q3qO>p6٧g h!Ϛj{q}L?)FU0d˾F/>޷ě\+3hJQ3^]6 ݿL' x!sz)5U∲CBx[}|NVRE(U}[P~\l }%!e>\Gqi4@'i xwhs Ll/əEVy?PV c<^({wƣkސ  "Ihók.,R0Xo/m2?`ݤQ,< )hhbyv: K"7_˯rʯOyr"5z\ֆ7|qO?/*p Kӥ`GNeJkyrK(zK]gH+MY) '..0PxweJVT)̗^/L+&H9'kaXQF>šg0ۜckЧk AH)?Q(q <{DTl0p%]9Ȼ?ZD2\XLa=* =@, %dUN0ݕ#Y՘I#-nNgoʝ&\pWLU>x?,14 0C YKWOx1uͩZszrḶkp'\(W9QoyHÐLE~r:}*'94\ZFѿg1&O(a (PS185}&Q kez6'|g=1*3Ɛ,{He,Cu 0!zGnԱ.d\:Rϰ<l:r ʶ %:W 1hc^0]Z/}qoEbT *B;]lOym0v tjPR\FR"k8 &Iҧ!U1 #lٝK/ZHpxdFRCw!I6KMAu\2Qx-=ޅ(-d kb,|o`-@ !`B{LKb{֑`U!PΥ]}UϘ4{1 Ľ_!E˔:s ɤ°BV xN'󰁭JsU=~ѥ=T9>4{$ .Gn32ޚ+TlN_EAA?Rz*:@t@bVxIOm\ȜYae )m49eIS9|`h.0;s59@ -J_}Z)βޞ1 ~JL1tuE]~^xTCa3"X]0Q8= g()On~Xd%nnXѓ$1xRBS 9t.Q^5wᖱH(`Vluwg*E|yѐI< Z` KNH htoaQgLc`/p7m5UnbHQK_zZE5N'9h=?#}r,xw9ĢDjWBG! &cOo"# aMP;M_4gR/|R?p(?5<ƪ-67ːR&^l >ggc]cHڕIxztRYȄC!|PxV< ,GW{"3Apݯy]3sy+ms\|\11l1$n_Bmx窬ہr YLfJH0tHa»,{ɶZej9V.ki`vTJ~?|jT3I >$L]ĵݎ SGbA_  ]70eJEB":bUZ;sS+9%ܝOb`NɘAa/4m/HIsktI'PmفR72S:VRB~2q]raz%,I LUy_$e3R``BbkJq+$5 /Cꃉfxdb"ȹ+rCݚJ|/OEv}ڐp,/ /ߺ7oY?4,UM[, &Y[Sax=gpl%7pLo] fuAt}[H̻"d@0Fl|eFmYTiZ1ĖuJs*{vP}ԃ$B\%aҚM :?}Lɒ&nH XEHQ-{"GTZ3ߚ_Vp삔Hd]Mtd:Nx `#NZnLA{pd_0 4<~KXhKՅZ?a)I شiz5i'5\\˯S%\JXG+mJ(#hBy(G Wfo9rc)4 {䳲g!> ##ܼN *)WrIݴW"#{H'^hҬv*MԔ-8XE2{Gꔜ5ѝY3Bo,јi)c(Vs50fGp\DcoA@8+S \JWˬC.6W|rp p㼤Gj2&x&zmxArA|?ahĈMfء%cbdl3jjNnC8)-mv"QefwCP\6l evo0,9I)#h0j"l?X뗏xC %@98iqab?dq ^<{|D8*c49Ԙ9|/b/k_dai@]m 3G3DŒ۬DDqb~PIKFO&ZRL@^b4&q_T2O&6b :P9[բ ¶[uSJc-͖cϾRK* 3>R۬E(^KX;ԧ%2 Zb{=T(, SR mX@6shSpמ)<ˡs]^(51%) TQU{(z&duojnI9,\V=_ Ŧ0-1L])jXԞK z^fukLdI"XFk6JW򌷯/愿U6D틵 mpr$g7;N㞞 S14q ܤR,5_趀=ZA? 8ϱ2X65̨wU:;.@H1jҏ7V#{5C2E|7sX:r%r-2w *br%LDrk9VVΟZg6/^GGy馘j:Q289&0my?p|yҳk`T~ 3#^-݈ENp-b(v6- ɘ{QsD%+Hc>,{h YTZ_w Rτs]lg5|f`|8?$'T/V:F=mIdoZ 3 HfsLkւY&HiZLtƷDp]UIs$d8+dn`.+|]t/')C&tda뒽?ʫ Y\ ;.!6ccGU'P.뗘{VGoͤHw?IS鈄岞O}|& >%WAF+ïЧXB1w9O!٣L2Oj7'PlT>kN 7E=?]#| s1vwC/ H @ӠU69NEA22 6x*0?*MZLVy+zb(?݁y36 55<ߌu?Ɲg9]<FD |2`E^;Yt?qQf#7J*`)k1#:vw,bIAWdM\ݲ jo^"/0/&OQ*d"g?m ioim*rqy}73y~"[~<QTPY*h L g|uC*[@>B֜[IveGGZQFzY7 v?J7e۶L3L On+ c*-6:!d-Wo @+N&Qؐ^L !Ncݏn-J:oKr[H70F5ۏgkçq5G@hpIXƺ)N߉5u=_ѮI4"5xFe1 * u'ak;5^L<m7u6z!% #S̤*e7 *o2Ӱ!֧H$jn19EZ]&AP 8^TTNf,īN`tqޟRK8fN,_Q+ɱA#Rw[80&H0"kKԄ_$O%B)b 90v?6#pɹ"vy/[[\Em9m< ^"ճL,H ~2dZ\W1ѡe !XOVP3hؕ?@x&C#ŝto"fsHo Dp)gS,v Unk$ ŃkטG$%E"_@2[42@)>*W~R㊞@Kes 1b o@gko*wIܒhl~{3o(tGSSPNC cʹ|qKOmܸyddoMMO6yVj&ٕ0Pz. uUR#1 Bw!Q}e?gp '3↼xqBRJ?u u J U%g"NͨIfUnerhV:tK@p}>.~E :=tfP؞WW4S+4y4Jѡs17 H&z׾-9ʹ@D#D%v՗4:ccAc@PSQҍkp 1,޸R04}<ȷvkL$$~JV22'K5"cf9`&Q{\k\lr쥥*o;gycXhKhe^\a{M%'_ƀAɇ->C@_* vhD NzqX-esqRdXJd0%bnU]MMg\ߡ mB.?Krq~F+m.@G*r/3yq/=ll2{lݠU#{eD7W jyPvş}} F3")(j 8Lɏ$nGNd zlo̽ן୎X{t_$i- >s`i̊ChLue1; q0QJṤ3=*`bʸy.9r˔bupyaVƲ'!n?@&װ%rʎ>#`֔~!63Ksfn@#Ŗ*Hk(Yh&bo~hџ;{n;'7;,oRͲ(iw SG+?Q1K$Y?Z5%Ys5gq8].Q#p4 dTUx  \f?gP2ĥ#Cm:e^Sͱjj91.7T eq`^GL*aoA6+:P^hb]kWLn~x+R>~z;eI/ )ksƶϜͺS(Dzs6lkhqLV9eZGK^o7ȗ? tANcߤ.p SDԡbjm}f=ěF"ӫVQ5CikMGX ?%1mm%`X+cQ󃁸0x|.!/w+z)swLhoD kn']jg8AdP:q8DY`FmnXN+ZΊSd?#<@nӓԹ_٫0/Ք=R:K#NvO$+~ <߷Y$BI Jj[*1 8?90e/K9*A&WJP1tbS@18:y>'YRR[:τ6.T%&=5UjY \sD4)/m{/nL~\nށxRqa{@CU(Pw~D9a~y[E]J nG,dhI/0Dt q ,^]02(tZ'i- t?P&)x(׀˪#ڮcJEOsʊ~YU~j-943|C&:obFl0 wGXj}#xr 9 +.#K]dtV-\ ϼ!pa,F)(vgB,S\"O0h-B#^-AC+^Ohlܧ T}_c cqiޣ!G%fErW/\Y ~,o/Pbt,[ePCfX ?xct]J+ ծ/mQ ?U%gȭN@+@] rlךv:NQ7xR-0b5T/E-x1lyJ u6 e>l-|(Vz6 /g4ApmSR0Xr::{ Y~^_MEa"ҋ{ƕK*(Oz@WE`UfP]EH?eo@)b&?.#ƌJJ&hbRj7~TTlmU#*kw 1juڀ7DzӿTsq˻FH215i sf>v}iMu ?9)~r21_:AxeAQޔ]BDPn©yAP_MpG]q-jԕL%N9~ptYwʫlƵYX ]"m|Aa!_.8bF?~ǜ%_2KAX)~*(Ue e#iCXvhs_z4T }r5xCh#SDU~0 EVe3BZ4@]; N!1 ]αM;KF=:H.?BDlϖ3>䋼&GoϏt93DB{빻Yn9Kne")va%C]w>9q_nuQ֛$FB)1lRA?3<ܭݟzDӉD]+]?}<8L glbcq7Ki ,RNԨ dq.pqzu6;3]ޞ),{̛,9k&ۍwVܡv}yl}e;:@73wF0_tOjipyk'n51 tԹ?yvrgfO=l70CW`zǙSQ?0zqR23U#V-9רRɭnq"e:Tߐfc {,*[ZE/a. /&-X3jHLLX=4&I>Ze_ WEM;6rPdIOr_)agr,]ɠۧlэ+RIQD./*W`+^ѾQ8}Vȝ^aߐ*-&#e:]WM"bWܗk5"{@SHM7SfJoCWiD~` ̆;Y;96=`L۬bNVQt's"?Sj}\(bQE۟+qH^X{o6XOu+jEC;P8%~Q =:Q2]:8ؚv +-f861]F}3NH@V,1E%qe!^9R~!=ɫ΢S{b_Pr_%j:@Ӫt&;4ԣ"aybons~{a/28~xZh)y ؁) &JXp"VTô,*ŗ\&)ORo0Je]d5;UQkz$G#=ʧ?} ߦ3AiJ97:T/nחsS0CML^unUjC+O C4z )'I5ON<3,%sհ^3,-%T63olN'\[S fK'm4:#Y*^˭!`Eս3"eJY3 ɪ)64}KgjޥcjL QkA(޼LlSX /jA6܏R+8Q|,}flَ,0=W}\ :pW*'΍wjbBs)M n@ o Cy)TxǙ > ^#0Mɷ$qچ>CGf_i|ڙ)=VZ.5䙪^mH۷ R~ y ypA'垗.tc݃WO3c$:b+q7?|kPnU鏶c?f,ՍT VEC?qºt}k,X9CD}2"ঔ_?lQYLL$ò{uVE ht{ԋ-Dn>3ؙm7hxi?ZәJf4w^0+9>;Rj`m($(=r߲029cY}mTny[V{.O&~'jQ{*GI]"~҇^d!_P/砢H> 5׹njK$4͛[ڇ-+YBH"`TXOCr :ߩ6́ tN:H\yѾ<@a-n"RaˉHrhuI'i{jeʐ! vDP~Vɚs#/pV߭`d?`]M–./.M5E}V<@JdZmSc6/¯I<6{繣q Vwyi>&V$trغp8D#y 4|@@5yeiJ[]sc$cPgQY:q^\B.S^j@Rr~O$)6$Gy˱N2:(C)k~Gn N @yIR= 1-24OC|>!q]$!fC9lUdmyٕ5) z%2c+j *nb膊d\̩ĺ8GFvA6^{"LSa!}J:J8RXdžv*XC&wH7%lxYtZ8W !xxfxРӛvJA!oz%1~klb3bJo|ghZMȐlXg\_H:I@RWSc;Lȸ;?ZRڂ?7Ǡu(?Mׄp0lˊ`9oz6=' H/'!sjVY}n2Lyҽ>E'\߶bfoåU9+KI`@I{Yk{?܏HκJ!h—|&SNmh+kTQt4>/kUj݈s"Lvֻ`YYoRt2Yv s8$ #zZӕo9 :-nEr94.>hgL2jnHⴠԌ}Q@ Ւc\`_pPOYjZ[xdr:4O .a.խzU|yƬh0=˘aP6ѸrT ?VUZ]*aM Eb(CqDD7ZņvÏa .$f RmbTRmE2ʶ lS ap z~ UHS4z>6io}@ۻɖ4ɰ !9B&)m?vn.I>Hx}de \CP# Ȇv6@MT_fBwt89}c01ڕ6%mU.k7 A}>:X(9<+H1THhdCoi[U]) g=n'pju!@7[K\hS(!xE-?'( ly'LYh%Kd+:PPxA pJJK$S#Eޣ'/Қd6Nڼ@>*Rg5%*vܕ:LV5RpvVϨ(OO'Jz"U oxY6_sĝn$65y~BlJiS]ƹT ueDv5VUt=X`ZhWL:sa{sP~џIu1ftaKAxX~Bެ8BU{A&ٻ4ot$jGjptO|dݪ]rJĜ okw]S z&Z;XU|"ŮV`ڔeܯa֬oZ@#9LqqY z{s<*La5_x\'`ϡڝL.[i,e(pK6uXl<+%p_bS\Y9*1T(X-~Ӣr%|k p}딝.U k |]wuz "fwanf>sYEY.j5&=)!oty)67" LBUF1(3pty4nՒ(u獏L,}?ih+%!I38 -4SW5dwaR<_3iitdLA^ pRz( [[_DViK?)Jiʐu⠔bj#vh0hD T/L?T ٕ=O4H9 0֛?a%ݞłFŏ_e)7kGn/EIJ/:1ܓ@ݫ.y.bꓰmT~\ܫwgWtjl@;RMZ`z즡YyD;.4 (tȀ]d%pvQhiuЅ/uJ!{YƟ炐'd*Ym!OW[>pg JM-edFiZiy^̉B2\Hԡ! 01CsY1vEl8#ˊAO{mXe%)8Z&"o"ಮ=7,,o~anqN@;iS%F ң&;xh,I_CQ&d ͒+ 8.3hF@*"K. lo?FS̋B$cVTT&GE[3 oFx}װZ7A;O aa(|'$gZLUPظԞ}aߺPv`tc7OYWN" 򚦥e6kל9qclZ}L/kx'`>@ r.l *ݽfGRy8T&j6==Md$cC񙼖Yt`TZy"deqYJ5nU&X4an b*}grAo@/mc0-C^^;GԷcWJ1 0v['G:gFn|5(>.]57 >\ &9!kVOJ67nT뿿GE9^phn!<  Wϥ{ø7lΣvJMuukW3ŻJk/l"gSI~ uKfW*"{^IEi7(F:ɚ9Re "I XƭP>__@*RT+2$tfdw:@&u:baW58sfeJR̅l#ro)%h|aQB\@5"[`HoMr=s>сu=\7`d"=E 3饲 Z@|H/ߞ]FCi2((U {Vj@!%$ȋH ѷk\f*!͊ LctM*,v 'zFf$  I3El` &'m]RTB7H@%Fܹ Ff&}L3@#D>7NQIig7*-0'=ٛhųQb@l 9Z?3e:#T *eH+rRv r?֭(J+&A5;o+(0eQL #aq*jb w$k|  y钚LФxVV W 3pdҍs -+r?5K\YJUDᴒu(vU2/G.k`I>V p(iX,23]V4)[^ x謹վF}Ifi c ɪJAj>=GB ^&sJ.V %e * #۱G7Jx>nY^r)Eg~G=ax+1HZߙ 6 &T07l/B)h*?XE.Kf'fUD~w2f,K r4X=M֜IinZA ͺ`.cE o_|g6W?ͯ==@PII#rI"GhDq&θ>T\}pfp`hU+QIeOF}Tx䩺yPNƕ| 'd3#RͣH[i R c6EΥL_&NZj&'w{d݃0qPZx#p3(#zB( 0yD>10}/xio8 bIp7/Tl`:{>fSH0s`qVCW|wB^(n˷a_談N&[ #r|89@$L7@V;%NޒN"A1B["d1=E(#Fk!A?A / Д; G/|M`-M| pգXVW:ΗRoj\YJZk7!c!̖fCr^)86>3 "I*cOaxH+D$_cT F#i6}p :**o7w4^ T^*_r|?*g8&m/iW/ yQgb\M)K;B{oZ21T-EslZd<"G_}zMl5}٩DI$8%ٗU{c`FFwl*2vn׏Fͭ#"$ IM܏;?S}} (}D=7V-bnCvp.-c~ j1Rj$*b`j#&3t+/uajQ:7ӧX4ocOO&ɺA{ 48u~F2 ACYMƯ`!<tƄ@9:e^;9#1-3l8s(PvW\4xeI"XZ|> pPEF v Tȕ!1;;=Gǯs9c(ύUM$BX m ^Xs 8^b?{TA_H<_!oQ86_f\$37̃vGE7+[cUU Fnn Gnx*;+[ "[O݁2A,, SwKD_\i],=20/g{&&fC8QaB<⏘paN5Mhف/ʟd$ xmuПWDx\-gH2)-zbEvGIᜲPZWo@KUu;TIBߗ] s'T׍e=,+=|!7J\Se`eLc^n=M2.1'R*{K$!{LEAXg\i) TF@:Nd?Yc yv XF#Hޏ61hONhy7p#UఫHаw]aƃm ܫ/֟EfIbrdcg9ӊlKtEZA%[0lk312 ^2'6 pn/,dyIMR4RRpKܲTk$VbVB3DG]aUrtf#m|*3IRuLh{+w`= Cp֓K~RLĶI:p' Xеr望aC`>F%YZO+ނD>;DtPh|}1z@6E'dN- '6 b v&WEsqD+껻R~pbI*OtB/E:r] =%"! vE aHc`SsK 폝0#H lNj.aC*'fC#M+eˢ׶ O=#te+įL?NˠH*2D8<0C:MI<6X_fB%z1<ȪQOcqR6#+Eˆ=z %?줎u?a8iQ5)ܿTg"7>Y7 4)a[&f 6e|^.Y ͙o~Bxf?0iT2Ŵʆ \p,4b.BlQC BIMuX? U0gj2Dss,!:-)e+M3v/f6`v0fdxOGRy?ky(ro:\K?)RVprQ)\_eqG6 t\+ӓq _)NXl oֲ5B"#/+|RHj3&_8S#rl`+Wk>sC繀0dEksY"j&;/d;%m!:DC" 9Qs$Űi Pމ8RGM(ݼn93t>xa" @_mYc[> ,sƭBx+6\`|9+}\LIB݅ԕ#0DPF5'Gpt/U$k:T4[ CO -?woK)"P5/ҍ܌ b JT;""s}ƬP m37I1^82e+{MMHrx1j]=^Ӌn=ƥ.F6.kְ{xu:fj2ln1¤s*Q,I A`t|GHĩ4ksUO_ atbȊIr$37?dmb/-ofS/ @QX<9ӿO<oځ)(`*gTymC'#XZ=.o C\jvfc뾑 $*!=#!iŮ(_cڔۋDs{DUDDhd@ZPyrꕼX qMZi8y=eWfi0_uuq=⌎U  *˂w9s \;t{X3\W7*Wx9]d0 nP]+)̞haA = N7_i?ClW(:~}xvel "pvd] کiXeX0B߻{IV{ ˚DH˄M\|e.<4Ɯ_Hs4$h}/3!6 r=-EM5:e&q08U-ݢ#$).mNdGlkϜ3rq^}Eֻ|Sk\z"uIJj|ydTo 8iM7kb wtm-[9{jlk#\+?v(aQOlUv >3dMԉ{{ 6wljbۮpdJazW;-o&O- +ڇ/dҡ -5#KfDrW[0 GnfCx qAIp}d#pBe5J؛4Nzry=x'#CңYHm@ܧcP9,B.~S@\iDuD l>u/Kc}(s $i;V.2j}L"j7>l/5r*.lcT-l6\t?=RA0 H75'Siw{ }&N=ߗ($}9aWN"=}e( ]}XwYo6e{{ʼG;a=d7 JA`R1Q0ǫi:9+3~Qrx7:Ή9/yBr-[%+-SXm◺5 }n%mT^׊Mnً#/5_cJN:n13jͣ gn?\pRhĹV]w8 IB N2g@˦-B`Y{䦭;:TL7/Ͱq| 猁px?ڢt«h54y9^wA Ǔ.5 u-u{%/׃Mǒw/YR[s|[_Y-vM-'ABNLU`P"Um`>z?NiY@=!(V\ ɤ7q*WrCuv$̘ -ѧѥHتz+j0c]Ĉz̸8\5$3(4\HzY|xu¥R?[O?{t6b"$ibsN ڥ⻁W%S17۔GgC&=7½Gq#»V&v1t2q] KhU;WSCY-^[j~/h KRō G(\ʟ_c!CNz×_FC up*g41"d XRzvں)A%Ye:;(po\D95 !g*:39;0y /mB8!۔ȞtQפh :<ȅ3cK͋Q'Am;?k|^<EM^sʰMW&yOiHėkXlB|=e;,$׽z1'"GaR\NG XW˦1gq܃6%ZsWjE$NnW21@]WX\WMQ##|JOj V{bEoȋj1L=q5_#:XEX'NɁxq?nG+|uɆzraT`!M&E; \jc1YJ>4QG.n%H}EaT@WDVE&U\y")-K['Y$5|qv!XX{j4XˉAq YEJ]6֚5ZԘ!L\~v]a?"%Бz<830.B;c@:wMk><_ժS~%*N>#vfAM[gNEyM1^wBPoߠT`ub`-6c5Zqb 7sc'mG݃"V}lO"cHϿXE36 i3&I> ?}@@^HC);nnLD85$yff/xZ)Eԇ !UyI2.A2#*ceL)bVmA?EFt>JHd?z-n;sAe"߇oMiIflNł BqCXoQcŖr uTO]aF=!0`wJfhD\E13|˗bm~uy[QNx-AO#ɭdn>C]bZ!GM2c]w~s~{ؙW-] ^S`HP٫l{8nY%eʎ\JC8"!-˞Hϰ6%1Z,W69ʛ 0cG0cDej@f:|)wkbJv=N愔]G|1fLZcasrvC_5 i\IBC"s|5wTL$ԾO O*N¨+jNHVWCz&i3=G_-tqj?{Q D wNxM.@";wNId*cfϾ<6ುDi@ +⛧3\}_["AS~8A(h;Dp89LPXrysМ-Wucy"\v>$U' Ok./Yb=j=y>&)WV}Ђ M,ՠFJFiY줌n[2}'bQ2Z p5;P/"ZIڥ[aJ[Ƣ,ݧ\`rAW`uf Xa9:\¬zAN&T/ RY;BS  i*l_`'|`jWv*R  TD"\kPS\m1,D:u“^v + zqq kc$;vW |ԓ¦vZzvkeo|[fJX^Τ|1V e_&cD'FaAAnj$kBJכ4H4: d|O"[#8zo]{Hqp4/ 06 !APb&CیN!ôs[H@)Ua zO56$7}Uv*6JD& ?M_'3?ȐNʪwM>Kee5gJ*K8!A0%`:?(" 8mOe0l'V~U`q`N@w 7(ХTD娶U{@#fh8=D".-C]Ł FN8Y 4D/J.g^oiیc gFٱHR'y/H}UdZ1$J^P̜w#dzエ5ApMma8D )Ky'YX󹥼Q+T9jD( `psJXNy!Nj~@u0 w{q4 jx7̹D :SlXQ4˳edrʩE ȜtQ K`cq,ҤmweVcNaz6Q[$[Vs.7 g 8~:p?A+Q*ƿ{z3{'~GͳCLH3+${2[ [T6DɽOx,i@q*,$_,-bԫ 7 f 02DNuaQ3E^`$yJ|sJ~ 2l"~.P9`W<ӗi(~)i).X2f{|3ѦìW: PIN lڑ}bdF.Ƥc0Bu 9/_BvH5g!UUd #\FɛvpqGy3ͦJrTfcĦ7qcJ`wtPism$ Mm65+3D8^Yewpgi~&t]T*MZ(p\M!=M7/nŬ>N wEƢm|7%S1|o1rUْ^s_ yO/P[d6Yb䒥j*"( R"=LK>8F-r xJR֨%VԷx!}{|b+gF@0Vbc9O޿2djR֥Jhl;?Lx+Ba NYx+gaŠkh71pq8*4rsaepE6_Xgo^yPi)_0opEnt'\d" \ jxj8ΝƼw ?=,m'pWBɩ{IĔ01ɷ"qǪi@n$`>Vjo7g蛖q6 >C`%;N2S vWauJ $"c;M& g  ZezՒd '=%O8N /u!)0ʸ3\F8zJmkĴa'b13QZJFSܻǀyT1>l uwHEObuzyi8+4֦`ޛ˛ BҘTyci 2O1##74 o'o6btI4 9z~B Ys0G̵taS]Q<4d]hx; O9KQg-'*۹X>zaʌ6Bs8ecl+Lyr-bwnfTw@9n_3Gt,Z*5e R 'c I0wK>CYvy.,͔(a|$v]7i|D8S%6kۥ^y&7p*j`q{ZRV<C5?FUt&&㾠TU Ѝꔦ8Ȁ$vJ"t__Ƹ2!MLI0K<+ؗfPR6|N%fW2 d^C[o٬jNJ  ۞r=jRK)QxOaeZ{RbLͥ:\ Jq@:da*$TvMOzB:$5?ªJ E^v+`NX΄vǑ_{6NL'e hgv g[HTG!?8WY T|į`kqF\]^Cs07v@ֿr*R'O6+(uo̫X?w$)(X~ BI06됾tOTW&h8Cqa`thL>~ݽm,LdIB՛{v2=^>XژY1H2̝֮ON<&=FXG`NwY=- =x Q\@;` L5ӯHc&R tDń,K4nCR-qIbԣJM_cOɅJ̗Ū*Q3ekqy1A;&-%F7 6Xtm? b5_&b2D@0[4* c,2BQ‚ ِ}0> 1?_,& ܅ĂA`%!Ц =) 2uQLUMq B>VaQ_˺xaYr;H pC=0SEAW=Z$ɜ̐qɚl߲%h]ZKr ?uvjm߿ &հ[{-#j }(8XVLji|MSh;= Pe/O{WdK,LtnI}R|.n5m#j#pz;Bnl/yXVlx;I%u6[F)'C@-480ý>)sM69$O s"Q|eP[g홅vE:N3) xd`g毦<4@noCϻ2B/<g;R({9_oS6Ϣw[õ%5)?#-چAF,W88 ʰ ) CkM.m.gG_ ZC soWD0.M&A#M1+I8%#[ه%=tU._w۸>p0o/+-D%Uihߔ].Q5 ƘM ?%T|` aX0 jm!ݙVܤ?hAH;*+O7"+_XB?I}wdz"]0ioӯaHhH Q,oi4>Y2+W۹6KyuEm-fHpj SVL{nMԑWyl/Nk ~E VfSA|RKlPsgT,f"ĩ. ˮ&WF~3(}ΘZ"s\}Hh="lRN֨V*|7i\'Y_B4q߶m~m"Si@A|d?l'թHի8rN+Ye y8a4[,tYUMBMrCFS|b ONމuVHfyD骔fbq=#T:ٹLS=5$~h>Y0!0cSJ I# 숟qכ{Bc~[hؤKs<2IˣȤIU;E>Ľbl)pi)l)JN!@G=JpBF9}zQZJHg!Jt*Tis$UWhһ‡(I^vǞbwyHͣ|LC/A_2c:MX`f76l_ƨoʯM?.ۚ14P (wONR_nX?XL-ц0&^~0KJJ;=z:;u4M%Iة!9 <"/`Q!{k8;6:&J%C-sb}(51%#qT$-M3˲tvȼ/W*%b?aQ݋B+~]vh3Y-De=b<ŝT)W RL"+ n%=l]w5z3A3)L犰 p#ɍU;{@I +(Oe@fQ\'JNv_B}h[np~]#$KqO䖊 7W"IIm2GͰ$N:wO3Lnw 4b[,f fWMe$[|ϻ3?j;-Wyr=~ǔl_ p-Ya0qieYjŖz'c)iZ fS@'B ͫ+Ae cۈC~]= dҸ!"jGW)Qr Ty /#ɂh(z*lz/p!zb)GI@mj]2' 9wB>3.V 5뇌vyjS\g=z"3ȩaՋpPbm" ƠLA NpA&^@TBpkSX"lNn{-aBnw"A {}}P3*rRtSSLj=^btKsZgrt V"[`0~9,兏nK~'W?uH0;+zKbwkfBWrFi ;FD6]0"8JBٶ8 X+ tÑS)&~7KҬswJsˏFz`R[·ӚKw?Ua1_:t7ȯp47M-ڝrmRcH+=U/0Ω]fhöYb/Nd1CBѕc?8IUọwauh#Y3S9] gԍdk E٧g΢S+`b :g)pq xv]GSnʌǾt<$ K*ݧcQ.{5Al]ϔ;U^1 V{C f4_+'pFHP \ CtLk,%ָ],&a1LG+{H6fOuTAm?gϖG|C,C3 7U աIS3)䩯 dP)2*Q=Mr"5U/v6W_QSNՓۿ߰*6Oo7-N5:XQ!oS @\XgNIOEZ;u:|ϟ Fi2B+û^idU0>/maȏ?X,Yk@1(Q:E+n/0ݓ?8VjM CrWŚs<8P;g!qhaV5sk"OAUuڲm!;sZQ]꼺ގZ)u?e6ȣ>{̫c!tv4KN5K6gZg;k-RCfkec?wu&vMywOVy|ڲ,Kqaye}StǁVD&bœ5؍%w gK)  CSLYS"lV?7+>9ZuuZq6%A>FLQKD. +Tk*fBV?B^PIe";tG 9vvOϯ^Qh:^xKPEj/T/GM]UQGW@Dr}퍌3\UjbQ3a9} x,HΟ|G$Sr-n<C-4}ngĜ/:a Zn7 uBpNrS ^Ⱦ8O`Ĥ?[ H^G|2ΆiZ/]811 zJD3;> b і~1`>Hj0nƁi`;ܼ.'Ꮹ,4*S ?y=оfdTM)W- 8e:8ϒuz #~ #ʢ]xNF(azS,|x.'JšJ^Ur¸'jٶUoJf{޾ w;U@02v)BY#4/0D6 1M~.NH^YetKvtg/y\h>sLz,ǼG8 DϛPӏ~Q3зR:j`3̮K7zʠ#%!IʆOB0h+wA D[ơLWc}<^S9X${bl79]*[o3R@߉(uqMzXK+UR]ϙ'NŸ:r/CLJ`%S$j ]lG-` uM _Oҽ'(O况u;% .vc'^j$I/n(jIs7e0 K1h\_:Ht/͏^ %z[rj2s/=r%Efv);K!s|MFX٤W ߲G*G6;,`ijqwp\S ~Is`.?_H7SdRW,G0]1Iy#a em5뉓A")PvqD8a4]%ʄn2o/}m- wYIƛ1 2Ynb;u]>s͇knŽrTrfd7%yw~DKxey5?k+1¾=bXn>)ppɜzF ~^K\\sCJUo 2 >&h"'ҫkǖݮx8ŧE'#}OhՓb.˿qU] EVS#ۺ 5tdaV@B2Ǐ36Cf*Q@_t~YUNls#p6or!| hz]LnpSNw@E2 Iz|^F.7_ei'"F,0@ vQ2~?㬁dEߌѫG}E)* 2.(VR6yAdRKW-:=9@j4ò 4* x$ZUo]%K|-o;2bw]4K~?|}ԫ9(}NwKjwxxxo`96 M-j=n쭄C+C%Ď29SOد#t !I#ɿN~P1=-<4LJDSV,i_<^p[j Wfǜ-\ ϛgi* vLAWXYg5o٥mu Ϯr! ibTOS^EC&STV-D&?*Σk ~t|*Q7`%--2Y?wd^C, ʋzT X >_@ 6ε>U|D@*Ig|$W5ף7ѐ lt668x.\vDhE_KZ|@~y _N v,D]׵p*N(/mxl  5Qc^x(9 )zV/Pfq5 *#:9puק3O-0Lil [3 } p/q~ MPrnk77NRl>`1'jo#pmOx)g\6=wCL[Dxom԰Zj!sO⥅>?3OmCօO>wN+US% 0ӡʤG` muI|,2 Ӛjc̰$6vz/zneS&ġmSK#K֝j_l n`G0`XޤhǶk?rAٽIu^b(&k2fⷿf+Zk: JxM'uOۏ,8 }Ȉsc  ̓^/c2{e[L89z!i[ |0N {h!qȮ\72Mփ,Sܠ.<Yq&uxCV$̴#h|M~`w$LϿtc'Bj̅1U) 4VbjUTr_1Hl}t:dmkj,8o[uz5N@+!0ݣNzGI5&LY@ {Y\f1S0`=wbyӳ3&@yK!n1s }3Pl=}P34_$Xc?+EҟJofCb%6DiCF{*_D&ϿX)Dd Nʰ04q}hPWSaн Z%OouK8حԣnK<& lR׹F0H= -M{m5V9@}0J<a) :H#kGtdtr*:wgiaDb6rGu"׻M(I1ܙ~ŞO"v<͖lU4v6b}ȵoLGDRW%k{D߭WjȄUb n.iz Œr90+)5:ٜj-'Q8ש>NQQPh=aa#@|6ySW8qi}DM@9 IAݤ"|h.RSèm.ZyiF)-fiB tSmvB gl-/Y_Ob~ܩ3h݉\UKϱƲݬa*XGbk'$XK^GiU_MrBH So~KeBk'㿲R>koƏKȾ&ePq1˙Smē.џE}.>X_^\)<otakB*ֆϧfgTw7~DsbQ2ҕ+SU\KQzƐLX6neHVt}ZfjJԌ-=4yZ2FQ}XkRl4l ְnq< V:T=ݽ 6DtyYfC7ObpgE3*YhI)(\.U LO J}X"oTN޺b2AABP J4; ׵|LŎ@R\i#PjϢIGMO툲4ȅ6Q5ط0T3ߝNq 1)X"g,h4یI^/q41-T}@h͏ A5@ƮkTWkl~$BnjY7(#H0w˧,B"HqGl wJ4>펙aGFQ lׯ)]/q7?s@L~:@ %]ORz54m!H3>ث%3eВhRGqp˓tG'q`Hebȏz@hܛia}-$q/ݬZ4{2{8<>M[)6> !񱂣-|'akd#w):.<.Y\Ǎ)Zi7My0.Q[t0ݦeh#1My 1uN=E}bt~L0/gz{'Ǻ辬8[č1لxx 5)<|3(.^OSQ yev*}cf1ΕA:eʗG/X'oG: ƵxǨ*%V=ر3uߗ:O\Rzgo;m7c|@J[ x%W\xs.o49 ,v!Y[U{΁?~uHz=y*_$'bϋ13n9.ai nc8S` ags1{r0eS C͸/ sQ"諚0)S~^yuI#rꈴ>N >{ Kya?hN<$v.hczhd0Nq}kžy(8p !YX߇Ҁ8_l񾰴x9TItߘGo4ʝ9LJ-8gX$쀚fX Y u<eǎc Sr&v4`.b#Z&Ӵv݄kOJ'cJC֘&|!#lBV0h*6:EO~RE^N 3`OGZ-Q:A{=sNjK.l8^JNe r&kkee^whčx5u,~ަk ݾ"EwE%x'YF)*ݶ"lJ1n"Ֆ}9oN9bq{t}OA,IKpE5![GU$,=ad4gكAG"?nҏϤ_- ER^=r撓zNCٍ]" ݤb\y<Xl?!nzC)$> \D,4rιiˋm XC;Փ&Z;ssue߶cE;lp3f"1H-<-c\H :UQ rXS5&heK. q˂Y".q65zwTZw]``FL‰Ȟ jZg~7;H!m) R|q%0u܁~B![afWQ͔NG^*De cP[o3HuґU/>(ȭ$0Uzzʩwi3t1f+Tm(9In>Rsӏ#̓rx^%B3UogAS6\ ?š0W::v {Zy4ٺ3R l cc$ǥA/<@eW529XYH['1p.P|&4s̸x{" G8~oi9Zs^BFdd0eCεb[y::4Q0N 5ṕjJ "@Cl`&:-{8Fo .'YJnԆy:ѥaj[}IIJEl Z8>kaB!5p#d#O221) &DX6cq. |ME\'Ϸy2c BBsKBZzf=MËki*dEqqj ՝t7Jr*D]'?Y{ eGUpBtBz<͆wd#F1a4'/3lQjx`7lӲ13}Q OuϩY%_ CR|"s]D>yԃ=L[[Cg ћh&C<0ph~K@q{M'Àin~ 'e$/ tC+F@jȳLlJ'hQs2N0/XKP,^c-9ɑ2xXn0ea xQf M}fBs%$+c?P1iȒ|{ze(4NY *iERWQvC(ʙJ'IC!֋)& SYbi-,%cTkȖF~$ DF7*l_\uӦCYM=Y3\+G|Gwk$WM c8ђoZ("\Ps tn>WOD|\ܰW@rAjRkcX;ZuCc2eMjDَ<+9ìIjjģ|)l X3M+Ƌj?>fR>-G&xNmrXpjCI_ŧ07N3qj''w{)Hmq hJ'Q=&C 9[b*p5&kR  tqQN{kiٝkH7*,0QN Na}9{ehOVEb/3g\|liY}* KD3ZR*ϋ@CF2de1!wc`s^gG»|\zF6i@9o{\@|KjG+l=jF1KPUw9OlsF⍑'>6yJ 1Y+%;?ֆ"rg&(kH:_C4sJR~29FE=Tմ8H+o`BxkO Pd6M -mmJAء̌t%TJk~}>l˼Pˌ'éYGg,7E%~gnFWKiip=}Q/#~>:?{)O\߀W{7~BcEg 2?.v;0j̗Zdjp@gkyr(qYn-jMn7I ߽P`CH|lU0wl0]Q$^OFͦ[xf)w}mt/r&Y[bĢ9+|-\}US(AyWO^+e}n"U+CM g{ڋcnEj<1~2?S˙vjvi]!@w.VgS&A68zOLҌuxY ꓤD^]c&EhXmG+0Xh?sXySq辅s'f2e:bm,+3$=hVsf-߬~U?Iu9׏k&\¨$kkP-G u|2YPj6Cud8z: Qm [?v8 Oč|̦WtTLYc#Qjgj?A@PW BˇR8BdPe)$W]To(yc<`x%!A릊~7П({w:YMOM㡭0+e1f gt~EV.=Vf-/;6>mnMX@}As3Sa0;/qg܃*xAσc\8N6?ޔix_5<~6s, BKiG^>zjBB> ??[gr]xА"Zh~ 6 0 KiQ25Υ.%?+j^ͰY1O5nAk/\۴jO4R%K//PXjC%.?wM\I7w{J=o|CbޤynF Ft+ oae_vEoso81u<[dEy^RU dJCT j7 ޺vU㒆\?/NcTLAa#2d,Ylm2Jّ/l@jyaiRRisM2(/[[乄ralŢ;.)|Brg-68RWټzGH&BcTKyK^_U*$O8dzh {F?"e-boe}^\#ۯ5I>#>p$dBF4V}/]p͉(R"6`ÝS :4AJd<7bU)M%U׬WGN67r24l;!ـ2WZKLu>8(g0ǐ`D\%( "k{-9CC3MlV@ ky*We xS1@z n;~j:VӞX9 `Z^UuJ?1c4ƹ.^}ݺR\4A ZԱ!E>FI㳀.ClV &|YVRkXȆ{2@x)o3J_dG{(,!O_#ʑę׻\B:yfn\|Y51R._t.Be}4Q&{u(W_sov їl*g!?F rwGɍ^4&8s{ T12O㗸au=y)#:x-fY=McE z>>Bdn&Q\ 뗪R8aV(OD-JK.J5lVSq { )NndbMPpNDqxR/=$* 1FҤdȐbk~*Ѿ(NZpщE\DHFdt I-['b0?#Z@w7߮Îv݌:ݳ+2W,-:?~{n55L AQx*zNg:3,9-W2J'ćTŤMsóYᏛJ?/s-" *e:sFp a!Twֆhjö4c(* by&"D gd&Օײ3V+x{6SDI$Qvg/05`&`uSrK-XC8aq&'QK:^gHFMy5~6|A_h/OybfR,$mM.}C2}[p++#|}!f E(>8 t@kFSHX䒊)!3u6yk >_`ٽy{ 0;W@mdbQJ^ֱAJh]jRgI% ! *ow$&a1[2[cڑ|6{qFWƜ5 8mkv80P>uͫLgg[ wQ|`~3 }`+Ћvi]ؿγPOp٦JrT9P4Yhz+vYJobTr,!8֥<ʩyL=>E.뷅^Qt I:zϽ~׉,e9< D4j'g#eYJk]i x9x0Gk,5/XhH8VoL0URi|1̎z!O݌r:dgoǧ`ſGb1dfeH׾ ;T*NNp_|,eL8KJ=6nrتɫiR ] vWe[YRJ!6TdG nXF:`[ ]fa65W]ZY6$vY{2an'1&w66qNf^BIg"~t9XX,҃ %/%C> $ZQ \f0zB%L5'bgoAfwvFo❞-'`6U՚Ƞ+'P Vv7Eta?ꅃ={!@wwANokA CR CZ)g0w4 RǟcCl{9]Ba6(v141qsZ!d˯,1͞'GChbGqPiNlcohҬۮ0po":(}^T۞xh3D5ܐpz/)\eI8ոIWM_kƄ%{UNC&ތ Xیs"~dy.('ե,5y֢ >g+wߎg ";!H:>hz=h ;io᷎vkG5~ (|i#~Ul\-\;YdJ%4=t5z* tc_llyskI۟BѪ2{9)f[sYVaYrpcPWoz>sjgX:j m##kAxBFb =Ғ" *ڭYԸ yֲ]p%' xt.*s+qdy.94vz2ѧMXKiv+# oV`I6J4Ϲ#+kUMJ茕#v -+jiPѐQH q4쯠 RԜ^_}čwg"Z-bbpԖ)FM6<%6rwov\b$ o&c 7tYYCo`O Bt8[E`a?Fe%w(L ~dHr4K:QԩՀAT'L`4 okF9o>K†@IޚEg2p#^O k\[ѨS4E9(v)uǢ=VQ4~u ^n_ɞ4H gwU)&JzpN矶+ (3@vŽ9]5M9V.pi=t-%h U_<۪۞'R"HCYL8ngxD_rq\^Hsuh|Z[h[IfK - 3~inpb|[/I)~LĎ9)u a[dN۔iO=-ѓ,S)h'QHLđSm~?H*׾M(naֱe'q]X `&M>4{7\-=c'wȊ/=\Xv1Z%<$qpJ~k:Y \Օȭ.LNl%ӠfH һ 2]9v0+{5n+(݈k6BCE _sLO{WU n|4lA?0I+D!>#A}}$ʧmV?H4toL.1&һ p@W |Wy7=$BKSY^),Wk_fʐ(\l{(shKP+ѹth*˾'.L{%gAI;r0.Xo-@"Hf} U}/ȱ, ~=꾢hrv艬FGk][KV}lhrGɌ`a')ϭbc:Ŭ-qf} Ym!F@bZ]{f .^'OS)h-ϖquU ߨQ>3`oxgGʈ9P77@\()jY /'V^P9(DKh:Hd):/y t`8YhsY~4p8p<%7 l*/*o #MAOɩ5KSκ#3-pp$x42dcRU @8LD/^N p=ڠvNy0K(3FeBMNl¿|^"Q%M~y *E4^&GahhؓHi7˳^#ˑVv#GPW[o)e!" *( vj@Jy[>Pq.-JcSq)N1FEqNp^rt'n%bөwp?p9}QqsRv0Umf5,z#'ԕw{o`KTC i.*k<#Pf+eQ* ҦbX2nQ咠 etpS4DVIݶ~kFgzU98" #D7b22&WÞ #C vʀ?U+ī4>vM,pWm3db'=K܊rwK$l1 P_gwd^^; ݱkkm޾Uk8&:%F_2cCp} c43Xo) - ̏ v|̧K.'+g%N2҅zq1.7gtM&gf&mE=>q> c"LA,jqATm`qɋl KMg;k, )qգ+}nPg۴C kMh1%_eJ _+\&zQ81K3F! 'Jpg6ЩNLPg`z~<|GuFp 9ƮѠC֋l\AQ }oDm#MZxW9)U5k"< V. DóVXi$IL#śpb*<4*rVېzB'+QҚv"$ uB1FX#$OC>J(w?9E(ck_ԙ$ϽzT*G3ұ.6—BUZj۶ uiQ\JH)joNϠ\Laߚg;Ż.eT~u7:o/ބ">K=وH.B?>nQhU^sc.Qxh5{V KYN;mKZC Ln.^Ftkst;aӳn^ifHys@VtQ vq_w{/kz>Ȍ:?XS`[1 coDWKI*nA _I@Pg~%PXKy,kv"U, w@Zj" U8,yQQPDfDtKzuIzv/uBKpp̨aM%l=,*_ -29l}.xmtօ>L*UqFNtEB(;%\NǟiS|rO'p@#\;6i +chWr~75[Q Yٝ- m8yfyEн d1{ &TRƈ1Di|g8~a,}0Q޽T32w,%(Dmvg ~˵75E9\gI^@P5~zU֪.(ʈ M}{j1X?9Kաcz5Tbs>L Ou jR4gORA9R%7HVlZm;ͭ{+ՠ`Eϙ|;Դ[,Ozft0X]SaVh3Ijڳ?@_d[vz9ԍc 0]둳/8.w |JĺSTR[m/xV¨)/F׏SѨw>15K~uŭe 9v`QkuȈLvs iP&S#@jM_K(ǷKn ^P2L;~q|9ZWǷ7]GT>:X1R:*|iMP +nRnڕfe~^2P*`RHNpFD d>̃UbDf%E|N`l ž7R:.05!v!D$Z6c 7Vy\:U(p:%oJs_­Yma)9Ϧ7sկxf4HɤX2s1B2cA~_B iqV^~b,4)Sλ~2lGm HS8v@:2rsdQ6<\#DnDMعo9U mR⦋ύ5"\3?;^u7H}=BQKnx%"+sʬvtA^ۜ_ GbZ3c$?/QWNeuE6\c,T㎻qghae׾%f:S#J;_jQaP qVX6,(z5}N )U` nYqؓacVDErrl.L rrZeb,g{(̳ƣDɤҟdQUÞPUR Z+8H!H].0ƍ2."UcF0KCrȟ!NazA;i=żPtѕw'wb a["U&v|ݳߨφm!iuu,=/^ń 7*pRtDuA[Hp A qZϪ>:ھ}RMceQo}S@8װU?/[Dz.9bw u:j2UtV>Mo>4fQpI.e%sƳJox CVW%ˍ5q֔ԌU2UL굒S*;DZGâ+>Rm8Xk7l8ք$Rtxiԧ"=)iSH Q$&vvQI/&YYԎ?y+Pk26̂a="=S!r\ml m+\f_ܕ #)b f޼9 [#WMENz)^Llv$9Ʈ4q3gwWf;(ȭyW-%vǽ4b}`+Kz 'DeVoS7WȹѪ'=%/M-}޷'9q};^CD%&5S&嗟\uin7|z$E,ոL<*NpNѼr^ּzW*=zauщbRFW~qcX|ST_&=Iыً q_'aARv{ |لy>]5XΔQC:IITT _S<kn10ڥaҮ0܈[NB[VzT{O**H.smc!7Ӝ@ ʌwrTFjn-󹜪MapBju܁&x+S+)9~y50C۟L%]KI\O_GʷnFV= |BWo64GTr`@lcՔUWcu\A&֜@[ovkLk Pv 3KoF?[V evw05q5MY#<6 ʜ)biE/N!{NذO@@kKu,ᙠv]\Hdd߅; Wrqdߏ[ҊXQح$)u/P75d K;L%!mc<1bp5NīP0Q__鴚ſsVqjI,|͍9D6ޙd)|Wz!U:YX *XT1q*6`XEk3}qcB(6;`=>0 t6"BڻL"yꉎby!jĽtg2߈[gbKCW8-Nt/n DrXi z@RčW}P[duKXne_]g(x))_7#ojU)^qucwgN6%5J0@֨+K?VV *W;aS')c.J)$Iʝxؒ@?*ψX${=`8!˥aY= U_;>a⼶Aբ}AW8_ZJYyٹR $va0zǣm7y(ɬq t}}/$t uH/IF Ure-3 pϒ9~*z(=tQ7K"5z@P֛#M&18Yywws& 6e[9u9/&v ɩ 7@9I*&0"&w5 E*kol4Q6t[خçqfeݒ̍?+>,$LC3=t0zu'|  ?垝WQJLF-9.:ٷ5+- oR>o|ܤuh]jW:]sIǔ2(Kn@4^[E* E2(P}(tm&n{[Z>HZ'"-/ 53h>#hR/ȦOG+'g:y^&C]?w*v n][Y!<;}uXgӶ"};CeՊ(8f]ES1F OJ L#$t# xsrt?~BJ1TT,>3th@DCypr=1T`Dr[D)㑮Y⥏%oy-RFHΪʚb7pD_(%3 N֞by4FV]Lkv[+۰3 A1Vax$d2dOl#>'rvoQn lZ=ȟͽsr+ q@қ<(1hgPh {M_)COrt%\iUqG<)ާ1 3MXt گI|z>?E:GGbo%o!>;y<#{6"n~q&\推m#o{X!vntqkD"^aHT*HծѶPj -qr8Ti)? R; +癭#?95]D8,nCSA_5s#"׻+Ru鲃L*&P\eFH3a4:L5#;+[d{CjdQ! YwZ-X [1`͓dnn΅]x RvʢSnX #nX]c 3mMݒgQL+nsyc`z}Fw6 WxEZ D$[ TvQ3>yu0®+`B%U=\ֿHIVA s>ƀ(ܹ{P;k螇}pB_OҼgUz( =2t= 4 0N0Vyk MQ{?^+YƎ㑻[al-/0ɠos>sC;E0 NWp`ǔLgGioS ̊,v"uM@o7>Ҁ N??b`9uZX淯Lj#{b7Ê6BfAX/iFA?C)軵Z,,!P܎h! N펒O1ͻJ/?JX/0ש?̪%[LC>ɧAF:uZkgdwƥtFӾ~4 {4ĭ jrEs 2՝~ho7OkZq+u#xwzU}ȼ߫ i{\K+>׃=50b0%A1 Q҅ -Žvɍ{WxgifQ3$6ش'`/čISY}](|a =G6GL6b1 VB2rn\9QW0=K1C`0 /qx*d"уx0GLP?Ieٝ!}[>)3--{r(\ C{9ƊkGӒ0;jJa7S]k kѻgS(wc O!NoicMŀKPŊfxr xxOv>ksۇ~ZXaRoo_GC4OИ \<J (G9S5.ϷJpіTflbEQ̔DZFw0LB h)-'2E9DjyETt)Eq..#hE+ލ ~>^&n W!2/ZB̾ d߿"- DǩE<  UE 5v7<8$` 5;Qgz>Bx| W&5U w<b%WZRr'QV'.Q ,qz5CZL!XM*6ym$FŪ^VI`PbIܯ -&H"x~A)[Q=Dg+HRM"Hcis2+ވAtQÕaRѺ>&,W lھ$ϜXlB@ZC fn.\.QJ5>[jw rmAy'*mނ@2e[;&m?viȿJ@b,cEwQ:yso^Y2-B!1.S- EKwvútaSj ?YD}W˜||$9r0hdI4k_-quQB%f Ԣ*73>sHb"jqDH2pR+bz<ذٳm׸Cc7R\QO1WsIq8 ٧*M0L`wQuCOŦI7 TZo1>6fݛ;)[%/o0~\'툷H"Q F1> ñ!1Q>(7Јݣ7y{[GQ F*nm`LyDH<@3"CmK>s{cOU1 rjޥh_>aя8fSO災<kC4r{dXkf;q\+Fo1kFWPX)g\V8,W!cakCj"m,%  QĮV u~36ˣE@ {]Էpy&ڰ^lP(0ctV_2B42TM"ȄW5~iφ*B))N[[薤U@eZhn؈9GJy0gܥ}EiKe}XC+ N"fkRsL`˓*3vk|eĻrAr}Yr'3ݠtqFwi'КWcyBZAd}λaz4t,1q}}[{_onZv($`nCSOӺE0MUK<-+*ƒY- aD,KuYGo9 {OH/T1g86egr #KDZ:  fpUiZU9;IZ&(O˝Yt҄?WmjWc޵ν3X?ՏRJK&KCxAs6Nlf3f?Sloz_/{&UT62J yďzv?yoyfgбUF%"Wơ͏#A(PV@3 b틹d?S\ 0YI\кPG~G} m\RFJ:V]ʺF׈͝nv뵺:r!% u' nR[N.ymZ[P(l 1 ɜ5wA,%vbSKj w_DpZ'* xt0_hK8UKR:ѴmP ]پYe7`PFw/y,eHIrP3Ǿ`OF@%2 /@t']%hp&FX6 !k^]|W!0h8,W % 6tLRcttF{Sn[s Rq2^Ѓ";JRyd5sya&|gB(,Ѽ&i*m$uaM"P;-JZ{y`FF@$12Z#?eSoT\#YIz'mjBg.z2{^vAy{4 \WɌ@6DwE϶7?k WpQ1uU9u0 wK 7xi܅4 ? gE.7sԿ 1 ?#"9kM Qê?_DnWj-M /䡨 g@Ks OFnzoi wqݐ>!>?N2+Jε!O1J~b,QCA}s!B2Y2YZfuZc\mcȺړ٘K&tڥj~EyXd_G1szY֞a 'cVoQZ(R撉q/$=ގP\F$i )98niy,/X4R$fV!N곛J$s]пlhhʠ]c&fz PY=#Dm.T*X֚n:ec,D|l}ՆS`o/X_K[a Z#CZ5i/CA]n #*n([ֵLX6Al?S " ђ }8d ׳{Q;}2>X1,/jbڑ}*8J7'tһ?B2˟ #r#cCTЍ0E>b 7[y^Fd<{܎@jD)ܳoBe )W v2*\|㛁%ok#|h &NBWˌ?d}!",-\Lӓz 36+zNRD8yVk8hN6%"7? cݠR| }<'nqe_l xVd.D$u2/hPBgK0z*OZD3)T7D`xɢmam #NH;rhЇfOG-^GT7?*۫s Ы)Q/s(`Dz"2|rd`:,>0g.% rQw _;4"fmZaеh<-Az p~έGgi  7›L7 =q)~+P5,#`~xO+3etf]5~iF)kڴ qLVHn##UR$R7+rlW͘| '9-[mԧf d6$X_, ζHq}1S67o6 '; RqOҴOX}ֳ7s\CcZ ɤi.&ZIף13EE:3dq 2(Uޥ (o-bgfSuJ г3cuA.6!ik * QV_t8aJds]jTX1=A*( ut_-E' J>)CvgHFubmc1Kfެ֌Y K~ȴ]w?u\ZQ!n3#!/6韄 cʖ !͉xwtxO m+LwY*7Ipϊ&j!Nq|):%GdՐ{ՉoybiIbvp0K[0/Q`_x/n(B#^ˆ%eЂ8fdž"Lkmu5}*aC1DV5ڥ+`e2#D[)MwS"ڭEl T<گ5LU 4j3@ 01ZYbX=c$==UF@ޮ/\{SNUg<ԊJao_G%iJs˔AD+co+op(".fԪTHM=ؿǝ{6C̿0ހg9%0ٍʩDAð*]aslnuΔضt evdD]xP +7XF26l73My*6~3m_E6 (#^*pHHmdz Q{ۿ6NP6S-IH'gMc0 *PA{.BBnB/^_y*Pqh _ێ۱HE#%|~`\[vt@ l??w,_\PqY$!EsP $|Both|+eX1Tt|*6-y,4k+>6:UDFH\ P6aH+S^DݕङT; sҡ_ 9f+mf36Wn׉ MW~%.)8@,24|zIYaQ]II0s‘/?#Ie4*AZNN):7H{]^>ga2iCu4+h ax 0Wd9u=N( Owa'ÝSdm< Vyץͦ68LC~+͸o4L%@ROPcXtJʀDl]őC^5a/ks)D%JU㐧8;'-3z}LY(vo9lswwd X|fBS #=A1y[Z5N!A2pg*h<]P(~z*XrHw[}{Q(;Z⾎lPsi; @up[7W)-]1AL϶5RUL"iF! z)񋁻I) },wOuwuqY1FF6L5NHHrRn'wy| Oy;R"w-,!Uߞ' ,BY+cm|l_h\K ·DjҙП`ګ\ţRB}IP2f3qml^3vmt˄mF‰f?Ss$y{4ļe`!me }z~WU!ꋂTjʕ<'=(kgLj2JЍKc @e蒄|!ާSsY( 3Qg1ʃ)b/"g)LO^HNp ~rLNwޏ垃 ٚS uCW?y-EB [+0Ad,Fۋq4*x> +aG2ͻߡrWq}3%#n])UuUa!oԫ^MnHƮɦ+!R t^w_#7@5Ŗ^DHB^"W]y6"ywGT'F\uұRjNey^M6k ʐ:x_p'G!$}{\ZQIVz ik1 ь>bLF$ڸ~8&ƮTiK^yݧ jRMu8lVG6c]|Gm=9_,+l8 rt:~ ]0"G3qNQ "X _:{Rcg38{l./+#8o@D\~ahlM}2þc1zI>d8 |qSz2FwC&uRj>1&ko˭ò η۔7ϰ) eÏ+P%FHr`=)H Ym[I@8p!-1xC6 /&v.}Rݡ] <^/*-8R,gY@︑V)ٛP "J_!i"ֺ/j#P[mn6լo u}I#ۄot;n_ ٔ.5χ;s&2qmT e@״6 ilhw4Ͳӝk4DEu!f0JQ^Xla'MnipRJ3.>#Ԣ)C?CO_drbTœ*ѯ_y+[O/Wy1O:89Uºiܿ^ygaY3 OMR섽\ עf"iHGvV_-%\Oʮ͉6 ~IcT}NQq(hI6_iFsܶN@J<&s;w g7.6xU%AiTK'-ZŲ' Nl~"}[xC/˷/KloZ@ ] +@q?S3<޴,?-u]v#-IL]a<8.{Ġ63η/:TmaeB"dG.]6Qp9`X4, aoxP&J[z莼|4׉mmZ0In(I8I+ybp HfZ&_v3-ߏČ`ۉheLi_P{J#q W|Yө>ԌS75X[$(qه86gqamWз唪yw n,聢<~9:w}Yt2u |ihcY):n15AH }hT>^MY=rC{ZӟG 3qw*J6*%<pZcSQ -K4\f- :jt2+#>xڠK,W<$y X~y|[d[ vtdĦJP#b7^2ͻ@492_[l9TssvïL,h#҈#,irt.)ۘ'\c zNj75']۫z!L} kCT$“j6 t踝 ξH:7Ȓ[ 0xJUùpI1ベ?H9Ax/- m5&M保&N +7/$3Lr9*:SwH7Fz69mRg@8 .Jm|n.ՆP#1hI}:wA KV{HOH<8xo:^n(e|F Ti4 ch;zI♛y3ZX*fzc_m3 s^{c D7th=ּ`bȞ u䙼|b]vbOųYE1ZU.VkA`F40ts>|_zFqb|h.Tg_moi{lWYɾ_i Y/ LX,EZ!{mAdHvG3X`  KIa]|_l 8( o"y3+13)+rnRJ989o;e`j`޷]_-U^VOa `0UZ(@W(o)R.eH&xw, ]"7,DmA2(3ɯE7_knw{A!8{hTsDm? &R%p|vT%u7XX89XrFw>%8}Haemj.캳GߦIZ_'u 5\'Eb 4DX9PڙAt#^esumX,t}vgR-[9e a!pel;QPS4-9Lz)|6qM).#GoMRǮw!b ׎jD]VE sx&֭$Tr!uB#rE;O{]U쐧cT{ J;d]nh a%eeG8(x oܠDG:˔`#\Q~;Sd8¤O:x|`l?3WD߄rRKPgfR_t 7jrɧFvP=Oƭ sNg9YzS Mf9qP:Vb`7X֭ ct:nߞcu .kQzYg[HEc#rc%VCn|eSҧ)I7ac̑bM[2-B°)ɱfX”F;S~5{=kŸ,,ʩ3I;b\ Hi!ҩ㠪sDR{f XCRʑtw:fAeUKGAbW[уilu_e %7HSI9wx1'[Cvcl}R"97]n1Iu.([ޕσHN`;S5`p5”jY*D L>aovlLi*ۡf0 N0RWn*SNCv)l誁!izDdI I 07ޅ۠$!~s+I7gor΅cd[MFښ]/XT\e0. 3=q!~|<*~ ma?*bVR콕G$!fxީ2KJ\yB{258G6`DI/s1bq^M\<QєD2vI;y Z8>#ἂ`mc8 hOΧo,vI}(`Ro߭+g IE<"ubLk-?)6995C3U_y$(7%ma,5{X$ȃ.}h9ur~hլ/m<#OZz y| { ۊ (* ,~(5lOqĄ}_J* ɤe5p?eK:T(U``2N.fTRb6vt :ϱpkA:"mGuBn^ԥ# $@cQ# -{RBTim Y)a廗ž?F"pK9Z4yE{`򺘘}IwLR{u*wM~w?`Y޸gm?_b?!zbKl{SApPV7l2)0XCTJz8Ob2ӄ`y˿:i?SCJzަQՕNt0m΂s2]ז()dm[smC&JP]ֵtL#,F̂{ŠQ#mLͤFː}w!ѧ|Ey5]# 7f3U h*ލ+~V\sO]d)W^6C j 5c4Ò. ]O??3æ,u{]"5 |?qg[="V, GFֲ,HKоOHDw3.W~UqσąpM3(B:Kg4%o C-,Ndl REVl+cT ht SC~CKgE;IlmE}\)rhk|~y7ST= mBU:^:FY]:k]+ObbN՘sQ46\@Q[LBtlO kEQq ΀dX dQ3a`C:1i ,i@sIbmFx+UkGD5uHiClI iI}P0SIhظ꩙{/I_1ӝ hրh(z YzI߻@}2qpfruaus.ŕiڌo*;k;S<=>baR_Ӕ+T[MWYZMf<`DZ3zr'w|KvrbƁ %[CMeie #%ԈuL (k 'YPj >2ԁbqާQ +5&1"VS_!$fm|.rE󵷬?I#0{1C =Y2 Al6/ѐM-xTώdaJHr3AqLcPmcө327rڽ(k"SƳ˩NMk>\x{_p<$ vxFP}f@@/nLx+ٕ֞!Cz T#z$v;ȝNV^%Z;fkM`#V< '4:YbhW|CUfc>p@{D4Rgz5~vGT=7M SN7ϼ.+ŋ">ƃÌ*E`;{4f)iWQ|шE}yFrN.Ǹ;ł=ACƀ[ W'7PK6zWYo.K,lGXS*AҞ<[|z-&_#P"vH;Z¨ D"*vS? IBTI߃>(& ͏mP&n+9_H`1nag 2f?s1 ʇV ".H`dnw(\Κca--7G\uZ$y.XI`w]'L 'f;V0bB2x0V3sfCa/H'wש%7^,r|@x-/YMRDHҐcNryKDl= S֋qj $0I^Y <%zozG34Oux?p{17l9@/jH I/g(0n?΋1|  ч1/N5'Zꃅb,UyzTP\.82iO(!9D}hA4Z݁1#=02t\:%SwkvjX"|6bjZ~wI`GA@X<۾{ dX1FO[lj5@Z[QZnE yy 8qk?EmTTo^9V{kWԽ>؈x.:v&'KbJ#F 1dD>>[ufA@8 80zNг:k] qPhk&w+(S, GˉTvM:ʼn9= }U9W1ImvlsY( vܖEw~NqG[ 1L36j&Q=/)օ){Q 9ykk癅7_Bu)~r͠.N%<)!_|*GNxYYzRKQb2kW+u39ԦnM=424Wh<]B " &##s 8-Gbe˺ɘ9xؿ<<~lϚIeh|^rND`3op׀'qkVNːs '-2@֪dR{J٫'ŸP d-»LGR yZMh$meƲ]?wII+m@B8)Oj l(dC> =xXnTN *ҙS"Dn@9,pq1<{ qkҀoI4}>JUԹ %1eM[0L"xtH+8?e8 sg}R%!M _B̈́1@bܳ\F-cMFz3/ΛY,ZJsOAk;j! 8D_>nUo8dz9$JJ[.'Pf93x)[on(|Oj\U:kT-\ANp AҍHe}娦 >P?1cYu$HmQkcHOpɟӁOZT Kc߀O3f\@эߡrM6ulRF1M,< 2q Gk J@d|j, Jߠ7V>ZpxHtD]~8Pe"=FY9 b'1Or Aך>)ĭ%K:3C/'QQtNXAٝ<$j2j*!{vc%?<`E3lGŷF)9vzJ7ĠkuZq:z 5Q+u9u< 7?/ ԕ-iIyn ci}?B6衪NhNy2L5ՁxZNvL%ʿ bamHΡA;#V nCW[hhq|aQ Ņ_\vȟzΣg٫e#ŌqƋ6$kIeénvm h,c7fn6a?`q <&ʸI{ O5ZfK@\FZx]]1C #3Zi& 昻e]#DACÉ 6FC%H}\7DLa_ yKrR[Bꙿ֘*nsXF,GaI `ڏ2A?6!) !I>qkhK,#U!̀І+(jÛrtϪd@*JYF(ϰw/7;U;o<#[R2m5ʏ&!IT@LcEY6eʧvY+&ϩ.;@N'5 u\(J!SFGqG -,?N q=Z@@XDK&@Cr6+̆id_|yn7tF4nG:5wnQюjgt8}Jq&J0;مX h kr9iOaeTDH ! 07"(dMu 9 ψxՐpMV>kMM|7W J"GHRӃF\#J=fq-Mo.#HBX'a+[r"fF5Lܻ-Dw}8 SxK`]t`+K^6B]ݸeq`[i)i*`)@ڭ| 0_\.gUVWaW^T+a<2^B ; *ߓ8rEӫlc?-[t( jʯ84V>S/~zp) >hƼ)@!-#jHH3pƯ(I!։ Mej9@q+8˱R8'ljm=Y2OzGe#y !4ZmhFC2?"+hfO1!\Z1L8N-saB8BC.xO2*Ey"v p2+RtOZiuΏmw˔b)hk:XVrI]_d+RxO7o` z?Ee4W:0uIdr5 CDOLFLi8 V߉0U}GdO ~a讍!6S(߭vjO%5nI|=$L_\F4^~< ib8 ](i웖ڻd@(dAB\7Y| hȋ{mX!sZZQuHH' 9wE{;:} QLoDKXؘ"^G J<<}n|jA¹[R@8ඏL||qeW3j~o I1͛2N>{yMGtU#XbU}-ڕ RLÀ龭BnQE)&^Cej04?.:p: $3a+*6CUlu.|c3Y(^0J69u̖AP>P/Ǜ7ϒjUuul11 2Ǵ2閂%fsTkΎ- e2%R0dwKQqP!Kڄd˚Bے An{wp;up4nk+ r@deO hӯ]>Pk[ RIń49T OOkⳤwwx 4Y?ZMZE YA@uEE^o+o_?TFJW}~_x ^h! P6kN6=JN7l !7t.;X[867 %Mcز@-p2~ZIìȶ浬]\۾R+e(#c~q WG [6Н*bVs7j<\Vߑ;m !nk*gt’$-H鈵Y;Y3،Mz(j8ɻ?MTQwC[\P(>OQOE s${]Tn}vh"uK!m<68fܤu!w.,JfT$S0_Z @JܨyPA\A6z8|EHϴ΀/f4sMQ1OdYY KEkİV\U',(Ilֻ癩}&T}I4;>YH9릔A :Sg^ Xi)5 Yk#W+VNg-ҤqAoOp) KI:49gxv"e4%oT|amvQn^O4=H)>cmW_yenbnneMk.nݫDxNU?b/%Ar XO 2JZ-,ߠ b]Q #"WDzt(߁ m қzU1sI]SsezKO3N0wa[i1O(uRyզ+GDd̼fFŤY6} A+;ƳzP53ě1OgD,B:%7D7/0Pi#'*`MĹLDm\8=~QbV*KgՔ63g5 :fA 3,XqAuR2W #XMcz@+9ؿkU{vŶj>^mG{/T G(w'EklhH~F8cϛ=<9#9Ty(aƳkM 54mLxT<4WhQ`O3zT1j9P+ ?*}(4w?&[S3 ij/*`j_Qㆶzӫdiq|/BɇfaNXe{2نdrÛh-0+|$inI%ue` v~+S7c|vu NWKKV.JA^m-;YGҵ1! LB·sИ~-y@K`)+q 2i5h?r*%셖>$Yjw7VҜS؉kH&ٱwJ>O7A_aKmfpuE';DF EtFbQ9;c[Ւhf1-!v "H?sG0s~[[P%fw_CC8$i&)()c!b9 (lVʎ'Ga)DGOoRr 0JHr*q3v%;֛Z%Uz4ۆKr(&Xm'n-zQi_eD^|/>jrG@57[U3Xl>nX{LD[w9CΔS;TA7pF nчa)W볭pVe3p,?mw8ҲM#kE%BԵNԢ5> GVP;U%Wo9yP0k՗_>^B&Ш7RWE~ %lˠre)GÖHd֕xG9 ҕU+"@آPz"-(4ݝIVŢ?U39~U6Xo_){يOa-=NY7|0₡*\'Su} &]{$ͶvgVCYaҬǓ`<>.4i <[4&JgAf!DyœTW93/Ug^9x2Pg15 ;@%cYE*kVOQm}i>q8d*~!jwdK\/mm1&'j47Wѝ<®pG4^xIKI@M.їX fxF1MvJ]nX m-PuYӃ}n 㒺.X=7shD"?\ b2tn+=t6bKuzS>x0'cB)//dE%XNG]7y[Ԉqb22pVy ]B:,câ EjHrԎZti_JdP{_,y|6Lݔ3 |$LQ@TՅ#WD6} AWB+e[Qz,#0Q^E-M WMs_>WI)  !Y{ԕ=<8w6n(B^Oy1 4r#%r@N4Io9"/ե[đ\Aoŗ[Ԩ;f3%p²0>׼ ʅKu (,C#W'Y2F4 A<5!pS;ge/e>yhWU #N7 kP p){Ѫ\OK/(@$LoC>E?NŃ̿?X)C*[d]]w>t`,$A%3V"yI "`cNx ȞѶu'E[ kEWv7ř &1yA8㛵/w1qdrRʤ}6Ij<HWu;E_wПդY:}!ID a\e J޳'%Dz$PԪ+I m'm xfa38/%@|C5s1,^oQܖbd }{/^4Jm 7Ǘhj*z W.-绲CI9Xڲg)0HǬEiUcup*fn IH:߲{D, x>/ 'kX5l)_)a.˘,W HX_x}z$HE}Z( BʠH,/uZdvhp,wVMD㋨{3 #$j^ X%Vb2l.lXWkhj9v G~𤴆꧄2~]kT/B^c)L;R ?I|Fs"CJ*ȥF?rSC#-a4*H]"L6` *~T4b 8$z49ZJeNa{Wo1>Ǣf Աϊr;r_J& k-*YRg`@!5%Vgg@ENcd1idEr7z%fmFq@4,w@ u1}F^#j,}A?YY[Yeb.$k@3 5@wJMHcD2c"v:U*&{6y\_OpwvA{0cjCs[_ | \pG8N/|Q3a}6Bo^bnz_x,?oY~&' obTi'\ށt 77e^+:gJ]iWOyO;7Q4ĖT$8zi`"r6\Ҝz@4khӛ餮hTpj7%152W y>+YpIFֺۋ9*H[I" <}ZܩI 钉$̃.$\<8H t?el-2fLމ1F*6g|A'kdb$ڭH1[=L %9,,9e_:ǻcvB0T/-d92Ƨ5[֐ ,ŦPIp4.ovʡz `%bߠr5M^ >^S ;s "yv͊nd20x1*1 ?$n*uWBitUuL-@C)΢%0{^xo@u۹6lp< DgB-›qi>1LHO6 [tA?AXQK]^+!F-=uV}]O }w# D^'E1ЋP2W-Sn9-&wW:*8r"j(#/7 i"qf= p :Nvgi#z9nʸz5dP 2zTGa![H#/C;?Q.[&q~GwFx?o?E9 tI5iq~9O,.|â+}˜B;N`.=& zbERcWa[6Xq%5xp%rutD'{ C;ӻ-v<}ڹ0!sոtcr:i XAj"`IßOѐ1*pִkQQsmZT l3t 4il.;hL[^bEO t5rws F< "A HPC&ޥ7etuW׵Ōc^ɦB14JxN;' rL,zŽQôz1c}G ZuZ-VgbL: 4"vy[iTkqR.oشQv!X̂e¶Y`ԀE5zR=q3̊&qnHVXRCr#ww 4Z09 1gַj94F-/kqQY9qdyz*GKA <;A"Y ̖F˞RRN!-f߁d~iI yV᪚iɇVZ,Ki[u`2i("uȻ5t_>E]8U0 5I7?qc% 7n׹f.LݹQn a>dI`T]΋^6\띣"m"KQWQ%oJY+J-ATGMGL18tݮ/]J軕нP-~8Khj@Gs b:wcG]nƄ1bBC $9սât {$w•޳so]S[ Tમ1^~QM tp2n-(0'q\bڙi ~%`#IǞT7~$=B3 y,]gJU(tIT&r ^k6$DXcM 8ѫ%+u)psWbDUI3^L@Mh@"^(5X+X4d`↪("e&(U)~V8"9$:UR9-G23;A*Y(DgL>KQ @tn~9epU3"R= u1ŐKD%ξ[~9oTnAoԠڴ `:DQN?b W8HfQ7WE31J^QtM:A]Y}Han)p7'Nfy).3G)C!]:%Ve>Z[q:LedO~.-ͤp%E8^dSA.8MS0h5 ?QLX3F@<Wf.]d*X}p)?!} jjO$=kt@[mporY >rt7cr$v t'+t{P$Bk.l^;:AHjr?Plֈ=}k 'Cl3zg?$Wr"ş@mxI֐'Vw׺uXA z'Y_cBdE69'[%iGj/5hب÷1a y05+³t`wC$.F펞XjG)~Q!>I%#>:j&Pf&1!C Idte\-wZV/o4=En|R<:s QI݆ ^ ( 7Z_t]C&B{$1CiJ1{د-nm#Л hGCo*w43ua'dot=XýמH}CxWUi4*xzG4 SӐ8$S &ag2;R{Tib=rMʌf$u#h N.l8$FAA*ƑB@37k+Q b)pjP:+f44<;_CHM\5;p޹~ 6 b$I{F CKUMH&QA}0Oւ}ޙMrbˈʜ*J9\41՘^r*Yi/lpS) SJJG(˷$~,թ`A>8}'Х P),;Hx@ǜ@k_i˵!=2kQ~W.S !-:?|- 0e߄C$5+>Bבr@<ϧB7A'[m3@賘#@CsH` HEUȜ9IuQ!TnI>tw2>U?7g*<_淃Mu"{Ak/&9`>[jI7qO{&EH$1.vXdנ0I._v9\ þ{ᥔcoW.GL i&% m 1c<)Iڃ$/\jMr{ϼ6(&kOFU:')Wکۘl*O}^iȩ:<`< 7()Op&P8Nʏxhk7ґ~t?u/H`8 ̄ ]-;]\ʨK1 ^|H,#sk rOۡ}g[ϔ,haU7.IT:o7*`r 0NU q32H5 &IR!8 U)IYdRvM8Q<ݗY-ՖGmJ2>robN7u&2a(Jrl@ ~5q5aĨ67Q- ,b%g:3;?;D_P%0׏&:Dm=фş4<2Y"t %0-pycjک^pINJ]2oPx]N&ʼn^M Af1u*/okPj! }&ί̙˜{ρ8ADM#zFxd #d9!oBSEU/B]蕢w|Y7$ClƜF4x,K%e\ qAZ!#;%au)צ+p4r<_0j[JrX6퍿 Ws*vZPennշyjlHUԑ;ќ_$s &F3].͟a~e wyH8b,&7a'WbN IwT4h^Mx\=%3@tf k@Z98%&6`\Ci\! __>Xڮ?B(>8&ctb'|܊ȀE6з>r4RgQ8ajXn:cYJ:OifihtxjlQXb~aC-(eH,O+WMw9tzLp※0جѠ+g#{Zb3-yl?1fK>Ek > ,uKlvo'YKUmMyB KFw+YKajt,&&7xd E ji4Y-A0WمF6}a=t,,ku ]3FhoC|L:xG O[i~t 웠WDžr^6s}0TAdGjh@8¶EQLfE\K\=$X [0oDbI(L6 `m>p,N\ 4)L VCv>Z*1c|W?  O1D@r; 7sHbT³-6'!~y ~~s<%TU'zyji*J(|1ux=ӌ- vuevc ~hKmK~jEւ8;8_0X,AKgN=A ŜHo=1Un.<3sDPzaX%KOI C E>-{*،壍 hZbHXfۢ~eHݸ%kART.bgeXo~U(3(6tͫ ‹3qN\%?wӛŜzN` [ڋw"Xc~=E7% %kߊy.4ŒƗ6*x:D@k4ux Ҷ(A) o*yqB\(; dL~2ys'ɕQ-3zsp|2 sO&!y-;&$^׼V{9 :qW]mӕ LMLUBh<&\ߎdCMeE&Ӌ#$lt/F2KmaJh[H⌿,KL:.5MY-Ԗ'p_tj0oŮW"FDIÐj"Jm|gO@͇ZA!\4'+WwN;LZ6ϩ>zgGA Æm:N! yG.qoymç4ͶѪGWP(]N,ٔAЙ!678^O Q ` MN "㗚x.af")e tůasP),Bӵ,^=څk8Q&L9w)lBuKi$AOkm3O(r9^kv<11Q>ֳP6~ ӻv,`[Fy{YsprxWjశ!mC&u|0  ?BE5( gůo!ճQk^ <2Dhl8]ĮQ2v^b(|TxwK`i~Xc L ˯AHϢHIi(Qg.2%l}-]=M',GYGj/m݂lPW?^H,o4es۫aB0.1 Ug5L4d@ ^tJdކ]8. 3"tؚNJwYpᤜIHj5* GQoͤ{ s E 7.y75[2LI׭ qL 'slo+z4hiNVdVFƃU7ANCfb %.%rWN@eMVeLKp(QhЩ ƶ`T'jdGV"13v4cVNWD=.TDdƷ2uP OXe3{W^@va'ϟ_.v}ܷï_rA䖆0^:W|3(8o<-A!Ke1sqM/)@D 1ƛ|f΁:zq \,M{y̯~ jE$UH2<QpVn'ٵ˝j8F 4"@٫.Lp^b@# 푋T{}Q] wƒ)p95"9JZol׸RdѥmI \hKZ)7M{%)>]gZ\(]J.o-&2a TUL^uHJOmr`II7K곺ӓ bI@1D tB-ɗ> HLI+w#|n`3ϗs WYp,&J.ҥ%vTEitF0xfJ⾛!`eo¥[ l=ZٶwUS׶1sGz=;s|oY֢>&tsNehO[>%PHO!!|cFЄn#-OS0D:[QoӔ<fwʢrݷc,32@1/d<4aJ,׌7ء~ʸѰh|tj3y; /q*jPH^A2@=Kڿ ͂p> 4%8n&sr OxdY_w5VXݷCG<`2!jD]! 3* =Z!#OZ MR5&?^ti0T( JJ\Nal )&deWYt"Q3B+=ٟӘD\Y(\~@](ĘHWd6K9) »=^/2e"D{GYnYۯsOF|bJ2}62~( 5x ZAaSzSul4"8;Ui^),d,a<. c9DRQj0ԝ6@紬 lU#Pěݘ*9 PHp'nlhlI(l~v`_njJ`[k5Q(7؜/or Zb|ܯ׼\.nKJŕa_xw/V~hDut(D|S1v26]+]kFa" jQvK)cHBr|q6 dt1K/5if1jBVѾt8)AϮ3JGZ+$4}8,A 1`jJle1VSUrl>K+0QU~sVM RZ30xebS Aۍ+l@jV a %a\LD`:l$=@ϲMTvY߈R=~7E[tb/ps3b䝬D侰-yVاrf_n >?*JaIvgٰt_㬝-Ҏ7 sb_rt{:@ЂPlI%&`ER +* Bwh}]AKю IXl?uG(-=p% -@$o){_}6/f}WR!D5.m{/ DsFnK|dlMX i],/ZsݞY=Y;g@}k*n^I02X(@.%k|)z5мsSJK5?o bجtMp6/#X#A^:{xb\^6>q;D6W].g>to3OIɮ?SuAH( &V{u樞u1⍚lM]/8W8\Nl"1 |#4' 4Yf_hqC(Qnu~/Y)wjΞYG2RI@^Ib,mF\oz܄M!UodHRq&ہ42<Փq{)lFˁJt.鱕u4@NͶM]՘Clyf@*pb(`~'YbH'<o"}&v'S գ3k \ _?[miESyuGTm 5͍̩!@2Ⱥ jڂҵ7憾ě|_8:h[gtȤhU? Bn?h4pܨ2/hx"ܒl9xTqacZR(j. SS5d>cfGlGtGLש9v>~zj]r큿=5,Y 0@EbI{k96F${H$sRJV޸PYA#^|T%ebiQʏy&m)pOAai|^8`sa3 4y'aPAI3x@B] |)n h,\2FuG+ê YꠇY6fB;yK7n7ƍ>@7$rlۂD(.qqX9,ݚ1|Jk񜶋MBt iƶ#,%pXl+*97N&>lE5!ZEakR ahy=Ey& B8vIwb[1"Cرԫe Ȏ!ҬH&՟;ֈ83% -?O+׆H#,@rQzږEUw|`jETSty:&X(  lc,]$~sU6gtcoES^aϻ+e(z*Ez`n^ jǀܶAd&܅E@n[ Ka/E%!ʨ\TTKb:48?נ/@ meu YbTs֜m*9Si~e K$ǼtSᥩ`aW\9y#獗+{&&XubϬu* '{jN;2E#*M!\o[-^)r:h)icX O". !1a{n$_:lmqqLyTi 3Sz8]Hx'.=7I\2\ЗŨ˸SOq,&)3P,@1FOSVߏP Q h$7;A/S<}e)=8aѰE 4jQX`&L ky[2&cJ(Fn> ";H!RQH#.HAazčuh}j\!NiDzF -uG6֎;6 Nd;10X]GN(9jPD7Cn$tQ@Υ~5`Xr},=살١huƓ#~P7B?^?_ .(m9C6滆!|K$JspJj́ NN&XX8MJ "l?@p( eC(Ah/ϓsłvy/IPc*t-C?4L|]-` ̒Yw?G-9K/42Iص^ DA^񈳸@/%CC-`6'24pNz]e.Zw"`5y/n`q.A]bZA{eʞ)&_[v(V>FT$h#Gd_]`73VtIOC@[$r]If2HOVu^r]Νv?nRaxXR5$OQW6 vPuf3>9JCJu4Nz3kg+VH'jjΏΪ_󠜳3t=xyx`i& 7$,tHf[_O]WcrWVOœ=$eZ%_z(sê/HgF2L_\UPv$LV+|:5AȤېNb"^^T U7~k9p(i/Qtl=]ɍbۢ B=>~}8oj'jR]eքNW&sC7pUўvڋؿr 1 ;p#?Fq'yH^DŽuڢ32*[#ėDr# #> Jbj+ f"̫9j R^YM@;>m'щ9(9WmC)wnA)m:5n^<ۓ $ TgA!,_"GxO*40W% P5bFqo΃[z&2deE/9^hщ '* HY 46kܯDd<) ɍg0Ij ݶ6[XxM_v &?^TpJl@nO$."ֳlش`r g7`t0!y 4zƜa.G; Cn{gDg:0  h C]tۜ*IpUtnnv2kFx_<,PkuQ%MM 6_-۩0}^uu-;nD~qS UJZ*p*jڻAїw~hxdF5 #kngv9V3?ق=v@pӂ:B:\+25( _׮=;Ib1Vz,[ K;t]Mg(>/!.A!..3䬢w!!ĉƁ"%I&|kKĥ(lկA$P6򣱆#ih_e %iŷqPƉeA3 u|@W7d&,{h$aFAW/*0ΣcϺ\(ѵگ n c@P:YE#hj+!};$vSQyopetI?qX;F2 IrY%PFGY4~馱_E'"c!$DHym=acU!,ߝ?uz\['DtڽxFE>xN@,jw3B#G-FҠ1.o~gIk(FuKo QQF O</S5{*{[Dz TwE]1͵XDiČCHQhކ8PAw6|gu}yn_XBdФX ~o`e,SǑk#l/+1HѤ1 gE_lFKo#=e$(hYb;xO;Škn AXK 屼􌧻X"۠VB̓,:gvPūd߬'呞9]sX>̃S/7z-W,mjCS/LS &%dhAD`MgdKqr:t<,! Fm+J*Y0F2U1fA=4nREɊ>tƘ|{) u:c葍 #L/q%Q-'ևRI902<1Nj]V߼EK]q20ߚ˯cQIZ<l':f2yc{Wp2:mo-_k\ܸd `n|SX@AvrTx'>n¥'n$C7fx",MlA=*m\!.W  Ȇю90tD䚋25ub-'ra)'NC>rvW@əKm!E*ZGC?[6aNT[쬑E 8'JVի4qӍtw>lЮ|즵_yCycwj^U |nx3@&eGsQ_u>aYvHeЈp2ۃM?^t\׳&}Z=IY)p?㎨\5$3D} >H55OȕQKy>:pWbLoޚK%%;ۯ%|w^גE0Z?]xOMW :Vͥۚaڼi0ù,S+j]}NcT( VDDQBi֌6V0q70V/s.Lus^xQSැ ma7!,8K>'EZ?v}kdv*Hd~}g)8GBL.'0Vxv$4-q53H}{-Y LGO8V2ykoAGg-=j!IۡsFNDy( SJ6 E3IoWpS&CG'1Qb$H.8t cyF!LާZPF0/-g9{U[觾¸/{(e I m42aPjlP 9ҳ^ gmui8 Fik nu"im :qGUiLlr"Ö ?"Kׁya̮2<[B.qx|t /rVh =Yl7ihRBuS.Rp9B; PxFr퇿UWQXd#BsJhЏ@ wۡQ&aoq RIZV".Qԩk<$Y={ eiT &O|;^pjx "̉!'LKU!)- -c9 HtPUCd80ZԠ򄇫@*(AΞ,+uS|)dv/nE/4j# F\HY HNL-ES&dE,$,y6OZkqf 7y =?._tSQ Y3*[M&r! iŗ bɼ\RZ4F~%Wޢ"5 R !-UL9XYlpm9&^VliI$foJu2mHdr9n^RO(hYj9Z]9"}W眙UR}{A yx(+W /ZnQW U5YxO { ~(ª>FUU_L r|٬].y`i7*Xʼ9crW, Ō$Lc*҅ $k#zײnx\ )w! F;I+e?-`/ oMwKm&,?l=AZS1-R:gMm(?(m^ 9Tk‹R;8R,s/ ,bZ3G K݂a۬eifQbv!qw(AcG#6b4[3 1q(%?L-VYkwUmND zw9!IƁKq @ l@œLrjwזt8J)wǓTHCl=]oplISʚK# S$Fˋ{B1<|y/`t-P*))ԊL~,FT߲iE؃8gf,xZ7(GDsd}NO$UX 4E֫oMSAj|;WMe9_, ˖r\愕?=66 HDp7aV@jyߒńKN U|qKD ׵\NI únFgh+ls]LCCAU#%eAu&eXb0"b*?_B<r=\B0yٽ,?t =8#=9XSv',%Л6oKq RKZA=δ,<`2X P@¼6u9Wz>K Raj yd10eGhr '3/͈`,6lSErxԡdϯRpjK谼Nq.aSi qX~Q^9nV{& \t<ʜ'X>vY[mO NrmtMZ,]cć;Ą͑Aq<ԻC}E :P.f7D #Li9LA%Sj[`'gʼW"(A(?Xr~٠Eד`YwcpN]L('UWPP"i&*kLRqh(j/ VskT00U'HoqZXC;y,@Y-E3sjNe (O8k?~䴅D6n}AA$tθĶFnjv$ݬ[Kzy\Aې\B߿H|fxs\H-ˣ059W)W "J>MW[ZMCL`4P̌C%h %*rvS)9LO4QRA.Ի9hJ*'4V~iqywie8In-|X.Ҟd} a{φ|LM~{~:]I J|;3 Hq}5PCqpKӟ$2nCtJ EF\P$YCh_FaȀ=rC9qS⠬>k7L#&M.` aדoOEߐ H_Ij 'F 7+VW?d,M+[|,b^at(fBjr3Hfv;{:Dnk +2*`71qMʇ]ou׌"GAQ32GƆf֡~bMgyQz n@Bѣ4 F@) [H jඑiBH" D}=6B.W?uP8 ijtiѰ* OREiuz 2K30|eH✜6"fk'^wI{!`B`bB-ϼ q>}2ecӷù5ul>^"0݋ƻ-P5Ӝ(jiJÄy5FZ%pZu]w>(xrų]c#`bhuЉh3xM1g-83LޫLoMJP'uBԧ`Cw-k<, I~(]΅̫`mm :h?VObsú~=>Dh"( >}^)H[eZS"L' NzEL@eN ѪPpmYpuyWZs:y+9F1#,w:#c~; iIq̟Ce6]8ͪm>89V~O򄵝WV_UiZEjw8Pb=!B]!@}~xʚ#tVƊ~e B߻;k3vZF=p8>hIYOp/`8߯z jJǍ\",Dp)eQgLQmJ?X>1.^fF+'.yOQ>[ _t$1J6%7GmmJF#YNe#RɎE5nN򶦿ZK ~8j-x:5lIPFoKyD׬0 Ӛ[<kF^7~ 7 Nۡ"E~cͦzG.b!WH eme'b`.o{] ɿӞy+kR.-GF56xqOe`l?k( KјͶAë\>4-= 72[-'?5a);2CwGS|r:s4M^ƿ䳓"Xc$lV{lc0( <*\W/EB8K+KDVjE :? i%·#7Z Y紤z3&s[@ OG"[F*`?Y% (M D"sVS(MDZE-a7C@Xh*]!E'{pu3~r? F$n h?j[HJxV:`Ysw7IJ0ye]LO^BoOni#jx{\*.:{FNA1iEXf[ dgńk^_+L6|g޼6nV8<`%wL1n%w3Tzw|pU "zJwEHumR, `|sJ/MR-K_P<1+Z?9Cϊ {]ZH}6$:w˧upPHхCY: ~Fe/u-EgpNT'vb<&e &;*}0HHCsI@`oП';<~|)9&F 4Ýrx(T{Š(@şڲCVM23ď2o^AG8#,Qg$gJ G!k ^{-66zgW|IE9tAUnQWmMb+^gR%SMqN^~uoTU/sLƏV%*@yTzAyK_b&glz's|+_8 lؖʶ*!p/DXVjY#Q,2`3G[4ЕB+Zuc:ha:5=F sD,Nť=5ݙ4(R!bU^.r3\0M#Vc'a>5|-n4jۣrg+FX+@2&Drw^hM"^-9N~GF} &Bݫ̓8gm-;P9+)ĕj@ >nk8G_SK@)MR2^]G14g餩Qd ib+;04;q.O$/R9)4f[Kʠ!bBu!:~FeCV;TNG\P䐊KXT<&v/MI{y$:B1?+ ́RMI}}eڞ!Aά[Sz[,೦S*r.aƵ(RѨoE /TFQ{LqJoB؆ˊK"4_v'Mm922o)?>|hVGLCeNȝr2R7mrѿKKPz9sUUz,}b՜زJ2+%5/܄|}Lw3:f џ/N8X^["m+,ːlxɆY lY+qÚXHݖww.ㅹћ}ԿD/}4OxGQL3 m&:ң9%Sen!F U/e6 (-\1koG.!v_QrN>Ť+ZL6Y6Dpt }eyӧUaɶGyA% >-< I(  6&ڥ֬f7KQ`UUW"5|Up4f@BtI_ح٣!q?%i'< nZݿܯ@1#fFgj>] ]o(pM޲"lccFʐ.N;]gd2FҐI_)VY/ƿVKmeW6Fi-{cvk=rfojMr2Kp+guj~?ތ &'Uq(&ClDX؟X祻T;^"yL/eN_ Axh-n2;Ē2ke1 hL4Ӯ"fHK-2yxp<X eI!h+G +3YX"Iqng'.j5(>K<._jZLp:J4z/\n{,EZ&Os>W)QWQW(> ],`57>F ؔyQ 0C,c{eOc%y+&Dr}.m$R:;GOUJ$/5IGaȚWьr"5UW9i~1eczá4o18O^ ƑIp9ѭvoI4B. (%;Kij1I~G`Њo*g3ʖ Ns(t4}RJ nV|g2Y kh|Dmc,qXLyn8l;;Snd20epp.k#>0AOڂYw)z`"Bo8}cv-'m[f& 9źqǾP Pm8Ŝȕ8ɦ|03D/ I͘pcA55HvڂyRc;d`oˆ1ez/i)Q:+ eyY@+ .JȒpN:a ;C[E}Yeu޽/X-''[U4^2^hO ʔyP,qͷ8 ke``vY,y^w fs @@v\fӱy"& `xl._Vt1g~&ASҮ,&>dC_|v+EoX# NQQkՏf0]8)䊛>q^b\!k <1eϩ*{0q2^;&m2\Q/ x4g땦ß83TۚAt Lq\/:Wz+G;u%S4Ut&PK.𷍭Cnx:k";!'[XCxxi jZG۟b`* @b,XӢ2yGFϘ8~,hk)m%؏1)/P,`%@LH$%⏜m7˺c7 Y;hu>  4ۀbbp`PY<QC;E8M fB=:[u'֒ibE5%6gB{ 2do8 ,IRK`ۏqa7R 0<Ί8-˵hblmRH8F?49EmX&Z0kA5k:=Է;R8eXiI1vRZ/nNTYY}D5 8?1P7m~LoBvz#%sVWq` j/JԬ}Sx=y׌`rI,76I;&ɤynhri 67S 7 z| ulx`O.M"HG~_ ulIX= u O`=O?~o)&RJjL2+MPqETS~wB9Y!Nc0?=UQôQB'Gnk.7g̈6:H dL )aݦ=Jxsi3A,'Q!ՖgIY ^Jl}<%̇헆9P!\87Qb/Ϲ΃Q1S5pNNnpP,3 );.HA.$P 2I [Bݞt&PV5}ї2eS74j^x.KNA>X69_mpǰ#Efi`j.e.SDMCO $6W*1<~p/A."뿻:_ُؒs6 hSP's'\jU_wĢa#f 7L}ZAßqlo5/9‡>Ȕ,d7 !꣡`#X۟$Uh6n}bL-8+7/c|%.``aYnh1qD@fUQ)ȍ ehʉ16\ri ޫẌ|3Z;{r_M)fҸZ ǎK4qw ¹$(q7tP,/x7i ?? 9Z%"F+wo:t`xE!1zH\Ya݇_MF[s]}?)Hߓ: P JJ\#pX9@1VO (K:u3J):8^Ʒ#t6pq΁/T ,-ifգO U _)\ZM ٞ.Zr?LvK (B ErN:%W_m0XL*nyJFA^( Cb<2ڀd{Ck q,$!P?`U}(L}~8fKtAe9eQ YR3@S/<`s0߂ݏ:JdBϭn:T3}`tNt1\-Q\v#NZvMþz%jubUƽs򃣢+nXhzAKwdG .dGRV2Z8=\(JNw(>8_7JjCKk"&>^eoй/o p$âXgGlA(Ux5e CtF^ tqX@:x16D-';\;|*_l M,#^s  ?RךS(yG|@NiPU|Lswg 6<[Ha_wNaZI;F_9 4Q^: ѧ3 t%}d>Rt0zXkjt.?:M$ B9:adOcx]pc.1gb&JzVj4An=P(ed\)S^BH9"tlSQ_$o͠7ب"]=#fg_ܑf"ͰcWhWvX\?|rKWp;3X*gL/[6j#|/Q<m% ;HIgMxQ1+5?B|}&zr@5p1ØboU$8nv1ϊ#+MC48 Him0Hi8FA?/gdoy`(2x: ur2'3JBoKg1!8N3 !0t=VI"@k9+4P' |re?(5PpHor :'`]Z# nȆ ĝ#(ӶY6;*17},c͹٩tT+eww{V\Va-VDUXqԫ\Oq*LigEh=AF3eQ\3=߮R̼"  FRcg &Fs:!V2Ly0-$ߢ0Aٗ6fzp-k>ȫ`>WB@ B)^mpFAŔǩb7߂֧e 􌧋[)G֡edPNN2[ ө#`A:d$P=ЗR+ Og&A_^7G+iΝw{dUxUSgoMN`.kIZz |*XG$U]΀!&Ʈv$ͩ2*DS 3 ,^׮^k 5bsfo}#s++^SҠ'V; Q^tWU=MJ!J+g}WRzх"@٦ VN8$3&dfl/LyXdf 1`6䦅hW H/X(12s4⎂0MVw\iEI']^%|-c9 Hmt;"KC+l$NnC9:%Q6 K¿C|)G?Θ(KdߌHK+ަ0M~|XR?G JCCRx3'S{rBh)Ot1^ΌLO=*ڗ+6_,i~ļM2\]r2kG4Rnۥ V> GȇO:+.*C[5@kr4Z Fsq_MפTֺ0 w\3,d@ f41*5\rw=s SY|Ơ#.zvV:*t4MƥoCJ/&7+-fqmfH;S"{Eń] Eid"[srGHGS9赉Ow1--##n<@ji`&ծ=̂_Л鴚ɵ͛8y1g۔R(Kp%5C. 2wG`$#ҁ˵4<2p~7,"& ;}<1RJT~@datp^[ /Pa *>ĵ֘!kҹ\J"m+urmBzv>ߑ_ q~nazR.d/r9\]y.wP5j>6vf3dpo#q. C>vNz] ?2F#&هce=%^dOSSwԔɵ={z+_57NŽdKda  ;g2]¤\ۋ õCOǜkpc5Ԩ\N# nQѶ뎅FA쵉3^Ϩ|MSwbZ$OCb<)1>!OÜK֛yXQqm){CR=g+EV):0 ?pԡ"DpU@[!?I65zz rC@'!@N*vzGN"5üwJWh.bRWRil"x/Xe2Ttu٠̈9@7(i'/:=#$# #% Ԋn 3xt|cDo!LXD .N5>0ӊ#y~BE'xB3ط@g=[<*gj"BFx ~.Pe~h9y܃gRhl]mC52GP]KewzԤ]N&ţI L !}蚪agPm@~ h5NiqCܯvcӭ)5f/6}ϢoE* #+q>M=B4|Z c6\f~;p#\$R\I RRͮQ˖n=j~L]=Y!M7W2{ O8;2(՘Ne]:#7\88ӛ־%D% - 칿=P2LOpܨ]` }̍a38VI5SޖOC%mqnv!T5;⌒q7 5y.j`V3WR[wwEhT5e'u!]+ WvX@YB{l{s,PXVIp!`]w"쿂@ctmD&*Jb# q7O"A9Sx)餜OبUe+N#WLf¸8-wY^s)ϗ}ݰ04AB<5#0 ˠ00Ε7.fPWd Pc㎶-aν4Ԥ&L,/8'#@0.B8=RD>PV!8Qwd < m.jv}ltx)|Z3e+ίtfHyNa=0_9b 8A7KN98aD(ΙK5n_ 5kk 22UgCPP/o6ekTgPE ڎ[R5'- kZJ-z@ɟD)k ?(u䆩-[z ᏀVuOߡ\I9U[b'&Qԍx5[TK_ZڃQ-OK{[`{n8;+hxLCN9Y.n9Wp9 1dMlpUVKQwrB!8'oEMLHu"{რM6ݠ(*B'aucN{qt'5gMAÓeg!gKU\蚨jd;Z_g[ ^ФhJGy3/MY,ʦ,ˏ~%-))(kffD7{dF&{^ &Zʠa H:u~9hW7 |S܎ Д;UҞ NU6pN?&ofukhm 3z$١Cfy&@%* "ykL"dEn M',&Q]]Af@C炘pzVX2">V*2jҩ 0G(\8zB_[hJ$> O-K;}u9ŏP8xǥE ֤T?5M[v s"FKndMPW\ym꟩o=$4a;+C҃i\SD#FƔ`hL?0ʓ] I 3fO#Q?.)ȡ[ `>,60m߂)ס.!E jel\cy8Sy=ݐ(!'/Kt[׮?TCqP+#-m!KL& Br:J"Wp~x`(`V9Gi 4P#]CZiqց+E >=ڜjدZZt:\yR&I萐":·N)Zm 1ZW-Xqgk:teTAXV^]z={DVF 'YVq#yԬW'{]l> n w8T[WF=9/M$ilr0#,[8U{, H=ΥNil 髺gX}^>ʭ̪ @7iA]xU.dqBRhߜ vvEWidޏ+ˎj/{{9,cŷrְe֍nmWx8@Ū~.Ac*< j6u-e5ɜ )wzRVX׍x }eAG•vkD|F['<9 #:J)pcB~&CҢ U/EE1>8LP[Ozlqcw  LVD\}7tU=V8D5zuqS`$lWpnML8a~Ir<8&DZЎē'i)dԩݠk+%kdZjii$UmΕ{y$[ʀ%a2>63{2[vv`CQ3O m\_{Ȭɓ lT*#љ,ZG|ل`C3ů1[a|$[XcOWZ&p{/kĴ `(A" ĵ[{';9\RY(鍚.$Ҁ§n-l F2݂׫O 2?qЌTv!(}%Uphlb5cߝiV2XGEFe!9Eu!mlS P^T?9b64ǵp-^8A@ڥ`emzxoTF6͞zd:ѾY.C6֕N%hmj{a1UMŸ僩I(jfa(LN]e{nCy( 6NZ#5 F 4A֘2K 7q $bȅx6 V M"Ex۪ I`tw`\d%ALla^O&t%zŇlTB_ۖ;ő8EبM='e\  %-UV}7w&lӋ0=U:^wggoQJ?lcTƑ\8;w)×Bdpztg@:BrR( :1oZ)Iz*XO -q}+FtN?ԑKM S.uWj*E,fQ#S5CKf. 8:?83p2~L=}|Wu2l>;W}֦. 2aPkAQǹwXn̛.XKD3(??7{lv֑H/V39ayxugwÈ nAaI8*nɴjkmaŮ=VR?D:yp r#Շ og a黧 QI׿l:UtJOm["`ǻ{#yPJ4:qB(d꿟[TOalj9TՃ;aG%wE71$KEx6!;z3wi&ߏ|i=1Y$XEH0}v {e gq?GRnR1@zea>idR ,/. RT4!E(/gorǮ1>  &;[ؘZ 7W|Y5<ɢZZ9V;frIǽIu.oZ:N bN't2F2!PZ%< +}:*o@ XIEКR Qw{auC^aacb; 0b˝dx<./L-]C`%2ht2=|/2;aJӝcZlE[z*ܒw;N+s-|t1g=&&X &%{.Z-^t&ݣM#FA󣦪nF-20O uIak[P6 ih_K9KYpy/{؋NEx[HhGx`[>L"@u7]G;Ge E2;Eџ=N^eEUD1ai<$;Sxc\*ȢFeM X~wdVb.{;)I<CX9c.q]֒'xhpF09Qq73=!40*pde(ǽIa7C*_ZVZ%WZ:~X_eΩ\CV9V5hқLY7hπȻʑ\Ɇ?egE Ԣݬ3eKGS `nU k+][ _^#蝎$Q5i˚"\B4PVg ٮrSןgAЕ+3/r=KGߚ3Dv<99+rL߮qYQ5iä3 yE2[}NrrDQ%9ܛѕAZʎS=fn!)+`wػwʾ:&vz VJ]ٸa3*Ze/esuiiAɐt@Y_(~+xt;+a뮦W haym]h5c ?X~`fч:BI_؇rbTzO -C # l ) ^*UDnR|ܚҴFf{^_t?00%D$%mfXá}H3y dF dMF nh e|hC$j_c=R/Ru?%vA7/K8#~&ԭzu$˔HxX58cbiEE-Gֽy/`H'U;X,*~{[Yh Gx=<##wzQ;OWъ?z9`!J̀^a=;*]]ui|72*!dH$k3shHԿJ\c4E !GVx7\`B5ѕTZC/ ZuB~H|*'̠pkv3-0nդk#b4ckM!\n'f\( Vr: = {E;Zn7Mu/ á#cXRKN:N] 2^@>G̜V"'K,E'([)'@ $Y>N?7&<3ئ3ET @)ZYuT қFl،]iyCPX.*z 6"8Կ u&OX0ѥWN]4A[ׁ@ >};i:.LP$x(Kxr7ݡnw!g2 PX Q8:J8sި T7MjgzcN n,Gs>C _r9E40x-ЬqR/ЗBɖyKԳz]%g<7CiV&cFc7*Pj24-ihb-N \4Hve^ 7%+Kg B؆`M^gAF^X_@ښF 3.`ʤ0TU3XAg *{Gl֨N( ( PJ竁YJ?cDҙˁVK '[Xƾ%woUǛH~zSWx֯rxŽlxsLBCa }!n d=K&Jߨ ~">W"}d90=rTSܳrZHΏԩ_8l)fRM趱a H_vV5xPwJwt΄ eNv@W!<$/ߡjĂ,}&EЁu 4)SB_W*AфΛ ?o_<拉 X-]i+H[KZԐ&=D3pڒn y `F4QN!٫u^v:N! }D`0)bسIߡlyIM\=p.yh v6NV/ gܨ/̟TM2]Vg68H,_-_ ҮTK핂PܨW B;#iNif8O%XR;F[oyCA}+.3m#6))Lz; nxC!)챵%$$wC ]fk<)T~:}^aeeϽ5mM˲ck[d%oГޑ<(&X>{;$ɊpLz=D & ost t0'mzߩ@H x5HHnlƆ="7pg'̧죤*F5頻{59MRs$ A3)ALpI:bydd_w<kX~;:OJ4=*NN Z;poq;rHD6eQ9kyRiaM#.\E"`Z>gRs#mȌ$]"vGMaG1>G]e FW*yv2pfbua 'W5pv<]F|)X+ 6 Dt7c8eQJFZ-RTI'lm$F4850v 7r\ݹ/OE2:_96 FDs"܎rZtrlaWs:Da9n$%%'4l͍8chinE8 ϠB"I~ѫ ,;rzR 8W=B׍1,&y2 kKrou IB>Qȿ|<|Tp 4N߫Δ$Zء뤐{Yݏ:FsL(X5՘V3&<-I+:f! 寗w]W*\_6Re3Kk}j/ʇ(VGMl &*ɞi{z\ݣuU;nLТ6]4 5`(zZEDbt'&4UL$)oR\̴dCՄAt?52yopмP]G Qnlx/#&S] Z=|)@)r Z"YV/MGlZ;VH>+-\iaugn|k9\(Ҵ11ܽ:h9sVe#Jj~=DtPw|ty UN\hE:5 1 Fer!Hu@G{Z)v!MۍnpǯhTƒL1/+uF%A}L I\ 1O#qLyFJ>[I7HC㔢P5bmd9~d(VM֘K?maPr,}0-n xVv @BR5w_yϱӲ1SoD o$r` Gd> qCiju$"쵻3Ήb0qJG ?]jZujpeK_j,\Qso\5A؝ڡqqI8ldh/ J&=eZh6c圼?bB< cK,)yY { yQG=dN^/.pصG~֞cLm~`KY99Q<$Zl2.5$Lf-™b;ʿEŌ@ (Xx[vdd>IaGCErXSUݯ)41#TA+cN$̓p7 ZTA6~эۃݝCɣW=ߕ#l&'KyUXCqK!*GTi`G]Fo|<*Rl2 H!חjˍɟSÝdNS[tARwzUoLbw쮲,(.jj("ahz/`(1x]ύ1/oƍ(S^JF)0 W_8V\ʅZwAt}ڬP9Aouy7 0.n0Gz|o}YK_K(az!k9?o~9] ֧e(Ύ.{R tLCNe* CĒF+ҀvծذWYI_0h3;\o&%g&mw*x6"C- u>Z+9\<%傒S3ztmTTH1 7!npHcBk6]O“sJ԰ށOjwS%LP5 > dwz?aD\Yc||O*eqOg,-s9/K<Z  fWd5@"_ȯ1`UĤGI?{OVc<ůV?GMz<$5@5E5KzO4F@Q֧`T\[qKCc/wƟo3u M9/-ty`DCY^0 ֩~OC*Y$ʓ .u ," 1腧HK{g?l5DTK $Sv8ڑ5Rt;2w bOܒ,4m 7Eý}wy7aNbЃMe _HQ 8dqiCk`JKP&C-DU|f:8Y2-.zzF[d@BY [lp⥦ym)#[7pT7ʍSYDY[4س3O@kʫ+%* %7l%tPr {"܏p;nf )džSԬGF.Vō>E0w2"rmr|Mu񷷊 :4aAPx[uRH8O(S7JwLz7]]FC5qzH %bE:ܣ$=<2n_+31{`1Q⤆@Xkt@w| ]g9tÄ+n]k NwgOQ/te< )X^x0 Ha\si‘BcbpwM3#RjwW9`E=n3v} (\>\*?COȝ;6 I,0y_BΕA\d f" %䃿 W'?' & JgP:]XXp{$n/qz/\/gJ5Ll[c(xyjBஈWK^eY3VJku_6ψLnGs,W׍}$RxRpB7oF尥CU`+qu')8캔͕fJp7Nͱ n57M;X\(iߧ../|CW`!/:F,X<Z`J$N Ư7 'g oo>׷ }qkdL8JdW>ɡ;'Eu2qsC? ,CMO\USv h9y)j@GJ sE6U'$DR͇ jB1ĀkPÙlTU'h~&p_4Xۖ6޲͸Efn۴*Dsvئ3D`k[ ^$.!j.=AZמ % Y*Orr( J9Aof, Y[iY&J\F4*= ftM"v \s? MGif:*쏲*%;Z>1HPRH6SY='T)v6QKi0NhU8F9DsCD"MItDXY+EF@;qVxȦT͘@3B8Vu!?s]hn 3,81'o<ԜeOnˡv/i<ߒ:qk;3LvuRDk֡ai턛,d 0mj*N]BlO.HJlV!]t*vqyv4< ̔U -ṧLk>(^]{Ibz04,hTt9){,3$NeDىq?ʯ8GNbJΒD'Č>uK49pᄱw Zs xփy7 A;K!i/٭%QPNC@„9"E5/swV_/ p1!cz ,m,-O\S˧'sg7<M|dDrîwswSÑ#݄m^IWӢjfW Zh-jN/@Чl_;lm0.`)eRvۑP͗eÅtl6P;s^ٵBXƼT bAEBq dύ UW7(.0Yje> Z;S2& ~뎶tp3P_KQ{4WVw4駙]+Ԗt€X*1P&{TXZޠm-y\:/XvwUx1C$KJ3ʰg_do!kzLЙq]Y9.{3S/PUtzrLTιUp s^7Iٌ[~n.ub{BI)@.Vۓ\GC t1,yGۉc%AW5q߲kU%m EA$ha/?vo5 UD})Ee(N4.]E^!:7,ä jdT]siʦ9*HQoADv!b2¶ݯ Gw,TYP֒JtkΧ$0dRȢU$gHʰC!` UˊA zA@+Q4E|M"vX*QUPng؋E?TbWC*:w[ 3!G+#2ձ|j9l)j/>- 5vWo8p ]b&X+ F/3ۣ<G*@@Wm#[3Zgn.4Y6xEוrƻ'nДዼ:%m-,hVʔ.E=0} _cWk:8fR\bg[BѧxDx&YrW'j#lVN>p*g6-66"^x 4E]i J%Umֆ2U PF4HGRvE p?YFNKn ˊp=Kڲk",3T=.hz޻ 09 (."Y;&Rs`27T%O6 D"O󈉠#l(RILVC/='o@:apļJlm@?KU(|bkm9q)-=RW?o9Q~A2!ǠV8[- "*x]“;ly-ʧO8T n^n897-vt2ЍkF};eCD=8K}̸ЄgYid-Rv',wpBy"+rhţ BԇKEy@C|UypS+$_ Vs:54g왌fEoAq* -{ؚyPqR7[FIخMyUf&-BnLBG#RA[ov'8x}["WB9Mݛ~nD =b݄/GBkgcRqƀ1t\0? k% I,4=EV^XBJsdVb[t j!ֱ7P6.dm&;#]k idS/Kz!׮ !m䟑?FеExׯA=nM"#6xK^ ܤa&ɲ5Ye1 'ύ&R0缪7{P|g錄;Z'p< DeF$.q>qdURҮtF1% [0=S?U{1Hq"S;5nƗgb,m$(<ܹG%FnQW?Z>?vqBh~-t0v죓vѳ.F'fsXI %`ſYX+OI6H{J %_%_?y;R($uYg!*X\V8҉RRJ wezlH™MQZB@ ռ׏M긏ddm4u_wXER "6*AGj)k7R3\Xah[#Vax4=TD?CPjQ#(hŴO!=1CQx& ~߅G 8DA/IΉOY8L 2֯yZ=I׉0C6-Q"ɦ\.t!w[AtE[SbD4otZ⭾1h;heX%ZrDg0m@o("V,hsgT}B1/z 20Ɵi*詶}1kmw9d0?j}GZl3ɄU!8S[+/k Fu)f R#ϴ"6l O.<@n1T{Sq:*-h&l2@u:ة9u&}q{Plzcg y7#R*\kDl?yѢfрPxj|]&א^znJ1d1D"t˖OOt-eR;L~ V{g}5va*jB'gDjs2#rzj!ԓr!>;MM#R)L8vnQ VY Y2[ Zw hU]k%2!{ZyDWd (b7gJTnbiȲႥkT[4S˹:9=ؖ*Z` fY/u-9hEPn46T˯@ɯ_ n]sg6K!vƗHb fϰI8It m˧.6?m9"亐>v>;0JRR8h󞵕xJqYOWo4Ou2D9u]뇰hv$`E Sn)8E W7RC[PA4f'@NZ[Iqb[=,7nx Yfzjg㰂^& FyGjU5yqOia6?rGyq SR_FRVb%Umme8W;naglOxJ48N{Թa'y s4*V$* S\|>`, ^frfdݕ̡=}6eg8 4kDEA 6-Oo \m|; VJ:s#fC#F`M.+|`@5F1Oi(~]Ò*g#lSRGw24&+DhU@֠ r _ɿ5yN2CV_Q_$ Ci |@n8Tw'!qcs;x%.G'Lş+ Q/퉊.YNv{l6-)e X26V:xEQe՚'y$w7$0{(?9HfdÒ7Mm俍yd^:_.o bV g|59*hυ/R]KNLLM|Ԍ$@pd )tՈObJRȏvg.ܔ SQ!e);/ S,gG/C!seX%=h<,-Sg1I),-:nP(ߘ+l*NIO7y'9(lq򼅧ja/yʇmZIh>l @'hH+ah/pý1K4Sd2 {oq~d۸CREym8b"x2Q{S W߅č4 MZ05($?8tsL*fWw}vU-RذXp T?.\uJ$n)kS!PT$lY{A-(Y ]xߊn=.1E,4 جBz ۅVv5o\@5%un`;[ 'wQ.zWHwwz2RR4jeAiTuA3g7x^h$bڎZtr&*t\(\I,AD.IUOxb)yӶ"imZ OZJF_Zk82_*Qhi/Voo\Oԡ3Vn߯* /̕ǂǧ yBKD#Ry8wń6没z@B a27K7Q벭 )%i!:,fW®mtkmKaIS$"&mPh7F}<"wȒQ|shB( ҾaD`d|J\5E咓3fL/0Ow! 1 ȷChJLW o{XׁD÷[sް:poGt|s6oq H'TPU\wwM=e.QЇmsX9PUC0H0^N jaw' zptC=3PnWd&A#mlPfTJl:$y\Ay`)1j& ynìp%䣗61_n\رFrzooc+nWZXCc>∼G2@ͫ1KDPld^/{97/t]r$OXKr/ tYI\ބ˜.y^OM\c9!IpV; ܠ,t <S4ȫ_qAv/dHsFkԺngi s>_=U7cgek}C`{8&~ c%D"hEhF`5Ȫ `cKFF(q3U.r\ uduuw,މ$f' yPH&_*ųgDƳ~åW)oN1ҡS0]h>_6t FI俉B|^V+jC05}kEv}r/9|Vq&`{3ٽ?qs?Xny 8I#vԶ{V'|e8~@`tȺmឱM4NeQΔZmfvߐ~y?JYlZ@$P >yeVf]ԷAG1T@u.c#5kT1}MD[yFR?a*D?l3;vw+W].['QIӶxՒ(`L"AbP+d r*jQɄinT)c!$GU V!%]M\I.otSjC<ϩ0re\ =V"/FT?Yb^-~%) )h䎤\<ysMR Aw{U1IIadW)L Vx[ X`I0W7{t Hǒ }$mՖGRg((*HQuteR_ md`꼝X,&)y6kf7OZӝ0N(,-d# &D5 r+K]-6"'p2@rbEO ŁYGY0v`@ J(Jnrh=^tgɬxMXfy;QOoȯSb3jF.Av3_I2wn*LK`lܿX9JzRq?pT"ޝaU^uhQ"sɭ{6V!lJ7$) =`~:nXJ<[7S͝QЏnV(ba'|]\*s},?Kj(+ؐ EG\:%6Mp>kbq d1 >*N~Pf.R-䴫GlQ}=ÿ2Bp\Z>% jܝ=]gafb_9 i5*`a&*DzS dtI2*C2iەʪ;9ޠ\Y;+Q` wN-Si{0ݥ΂oJK3sgN ,zE3u[܉y.KLgQEk "ΆJ$1r͈d!Shڎҳ>,?_R%aӒ8ZdG3@_pKU$]5H!9NIꑬ?!ikPLmE['5ilowé.oMhLm [R:P/]ī 2C i w2I`"ZL3ؿT%FB`xi#NH/~[bO 3WI՚y;>oC=gv)8&ѣ̙>j)4l/(-ԘU|__?X9agwq1_[_fxKKƱ x7ckd*22v*us}[Aemowf9Vmg!z)8A a")@80àB;* lEs8 SZ9s؃)3?^ ]Rۙ6OIgL&M5K9&A/}jky>}\A@9~ 0agCFA_}rx"0qnx&ً+CYoFCi9D&W 6NrƝ(3ৈ4ҊPQgm*HC#ӭ.hbS'5ۂLn&v/gLKMkN0!DI;\ҿ`B(+@ Gfg3C ~f2?Pg)FXyFd"3"w(uVX(k\@S#7:A8u 47In`S>IP*l%a$a<;,HEwy'["66icjv""Gw?DQH:>kJz/tid.ծ,zuH×dI%ES |dpSx*ŋW߿y ^j`AoWM>fn$Ia*"R gL*8K+afIp>{^k\'ܡ3ṅ1:Z,7o\~nV: 5e52;:td9[D3'6TVKK `nv)d}MESF~k.LF$8 qŎ6:nr حu\@E3:iϛW2Yzwr .[Tn_`t6Za#ҥ):|V΅M߻qu˥0eyXkw(8=JgRi+5 eV6AC6Üv;e;a &rQq/=ӯ,P<_]wɷ"0+3"q[Y{[)ur]m'_] W4rNBZ ;F +NբG4٣Im-" XjE="FoҶr37lz:^ţMϰsgD;귏RV۽NqIUxIN1+J0'jll8_3LҺ)Yp0ѳ\ zoW7mC1#i/b-%gUT(+j_sc:i;,A@CE5₞CE;C^sU^.E#GyЇGe}ҳ{}k21*+U5jrSx=PEX n6ߖԓgP--Hn!չ}).C]c l}[s' fڱf>\"gbB(P~'bJI@gr3IfBPZ4O'Vb vS/Qؼ!_0(G%Z ,.:\NePh|ikA ՊG9\JYWG1:jE %ʻ_ru:RowtB⋝-6*6WQAil5T9 ퟙ">qm2ͩ/s#hj80 u)nMWȳ,u56{tZ!Yn̢[S̍T6[ɭQEyg.LHW0.3ѦmOxw"bZR5xfT!!BDfB~WSjchZ""h<\m[ǻ`pX'jq;ƅmɧf}U~0h o'[4j +Ѓ,5&؀o 1|sh\Uf&2c\U|XqHŸĦΫɝKӚFB0 hm~, 24#!.KmpOE8)ΘYXwWҥ|KP/|cڎ#0 *izǺn!3@ `)kJSh58: 4g^Lxflj\^F73ָG|'~ScKԸZH1RTQkB0{O}MS A֬NCek HxQHɼ!ܗH& ޓkV'٣qqN?#w`m$&9 vv=,^W8=)i'ƒL O QnL6!ruز\kqXOWZ9QcWUP*1SyO+*ŒtH@e 7{kEhN((v\5q̓.&1\t/08}7h,ܖEs61.( }f-+>7J(#3eqQxXyNWf|`r+iO9ڹp Hq '<5C"VE|1Ⱥ7^ s[F8p偪CRz5EI~r0Dnʺ`|J.(5&I-]Dӄf* vR/2>Bk@xAso mqoUd(6 5gIj.|ju$֌.w (7HCp?/<=vrt"Cb}&ӳ{-2 PsU`|%G{iVo8a|p}pޤt5>6b.ocEdvP>$#-26mR6O]ƴ|aӪ 7([&xWe ϫی"ȀX)C.>Ɠ~:?pW@xC?3i/^uC,t/Z~[QDGПMuXE̓\eh͊y3HrAx$ޗ>W}1i_i5 bG2ȼS/0GhςT@IsK$g|:܉z*7-n9,PfRo)05,': Qc4@m.&569 4c#sX4A$n;P^#“V2ę)Pyv[Ɖ=EMPз*۹L3Z%/HDڸJ- &}DKq`oZ38/6rK& ZuWJT+`%w.)_Q=!m$|B[ yA닍+hQau{Vv2q1>4F.{wU1kW̴i\#;-e.,SVI o`-/w55@u5e:lspκK~C0[f<{Wuf0Ɇ`[% nnDY?iM @":-FJ {XHM7coЊ0mqfERdl@ >iX(`yi#̣V',_X;G.VS M)&I1z5gy<6 !{I%җ6]?Z&kɶZsw=7Ϯ}R)]ە|hb< !Y$Y;H#D6uBl⥇pS&2Y Z5\'{gh3#ak?y} ljkWNyWМJ)3lX3IIЌ L[D8H *{HQ^x\Y6]=^Bg@M0,&Yftӵp)/"^h` 5toâ0$IJmj#~޸]$֖ 98v1$1 ^#ok(Qi @,oQT/J+,0 _)_|>:\KTwY"'8 Ao,L0MžG6jxV˼>tʌ, .oq[3\fZ|1:JttH#WVAܼn7$c VvpKVLja?A5~E@ӑOޟ{nXFn5cWwq# 7phԫ@t%mWvmU|`GT vKսp)t?;Nh.4cѨ\kTBU w7i~%PR _(#;Y@tCשLsF)ˡkn|ѝN /IgBTWa [գR),M:d%F"mݸ$n!UQzQv8#SLv]v3<6&HOEz%S ~Ss>E&삙?]cgOVUQL.7"ɘte_y$J!lJQ25&km*K2x߂ˑn]- RA9w7N틴I|A/P4e£t OmQ.`|)]>t z{7Uʌ?$A;UZ/&"f݉ґ{>[aByKVsFf mMZY }7Tuѩ(yOw=T|@AP ;5 ^SǮ^,SwB[@;j/b q2eZ6/YmxK\{HHDҲ]Iᖩʑ$RjNj"ˀBB+6ŵscfo$U?~&:v>'80@#LMW>~4̣P:¤ p~g.3 &H?뼰􍃢ghVf0#nR4EڦK1Nsg2*T5̨49Co76z-EF3DP q)Z;D^w $?kjh1Y空<TeVb= sv aM jjؘGuLsA owH$M|>N;c8azI`¢ض 8-&]vTa MC v'n\h#x?(MJǽx0Şr2#AM: 5k$3>H  >Z$INQMf0D[mȺ"h6'@LHHAEv<* !1'n%'|qn,HjEp`yn)pƐGRDqv$1p^ w&*FI2D5?2fϥI26e ;!b@ ~Zf٨*w*gKg BFA*܋:Vt{P\A^2=a1h'Qauѭ %d/L$z* Kxbα"? _4b?N$׉BSaY{NjQ-k2Sh=XyCSwgjU9膽Se=2"^䌧kgbm֞eH֢Bܽs 3ЀΒ>m:? h^zپjʽa vTfN#m VN"k2{9J(`ר"dO LN{RCVӟN߇S6Ш]u"jt~Mgd8kVXdJ/+ a3n >% M8>7pH:wP#2Zq8t1%U?3srR} #-8-5.VPT @ZJ)󖄶C7bʿEoDRSFx-|boQ5DƒM0Y)h "1瓠ʭ觞4/q}oPhGOC E.rvF#xFiY?KҤhn@2JBO= uT#"e'nJPnD{Y]V7hqC}+XQ˩Ceb8*$.K 4=T^[}eU%fY)\ݒLs!tZ\E:Pge"}eίoS٨I?T`s.w:E/L>$ub`uw?B: ?^e-^_"ii{y &Qyi :39 Ofѻ~X)_zP|?uyL&dEkMl_x1lEC[ǦGLlzB\ /=l^QBwC_6,,PtKlTa+^<` ۴ZGhϓd&t"֛{́DsΦڿ^4BFMvЍri6 E)I}TS_)F!0ƃ,v@w -U8nyb(Mu[d>h޽4hokhN} @\kټ11蹅3{T`ONLp)jW(xH;/8Aia+W6]YS eS Ѻ2ΒA羮| $[I' =~:iՔ 6P>!OZ FMꭂ3KM.dT;7a[`9C *ĭ=pDuqu y65Vq\N6x NWJ>:VJ43{> 9A2-cEuwYY" ~TV`*eܔWפ- Y7bob=y!lm='q{2PޱsGF]Ua5 iᰶE`vhbGa!5+)%Y8, D`!^ $wդH*gLQ4ԥsJ4BN+s?C37tFkɎ_d>?*mI2º2`%CvԚ!1ᚇD `0) BLS5٣뼉[]VNa)SMsXDgQOc`YJ-Q[Ӿh/( -I_'j &e}n0: ;R;G|8qn;>"PŚx'sw1u? ;qgA.oCc#=LH-'t<\#"9RSY+8Mڄi3d c`L3i˿9 )+Iy[TN D/I*{;tk ޚT˱jHJbܸdzVsc:.\S ~r"0n@Ȝ`ts",uA [uo;1EL"D:o)^7­24)\o/׌"\^ L8˸wk1.8 M;)V[X=`v2 y4R}!l_rtm21j |7Yt&ʶCyTlTBT5ɎZU/ܨlK;Nc('|vrwbiRJ~0Z_l/؞`OZvY^?O!XX^43&N(4(  ]7yWW I:sV4I2t|Z D\C)F}k΁'~Yz@v焙 0-y~ 5=bʶIN1+ kEӧ DwN )%KW{ (3FBƇ.Z*cWƦ;+؆6}[kԒ)kf-{M*e=s](v=lThh{[؇t)鰛D_I> ]YU].f *4/'j>% wNnF>Bjǐ`daj;I'Tiӵb|aJ+wY. c4;jO%'HoR+Jᆨ׌(ڏʰ-H/=ZIfs?I.[T)w7+ޞ rc_@f7w7Y[\ݹL`Gt~-ejcX4 (qr`8M x.BKmdH+6@&tRהl2lMUiBjT)]v -R x: 8<MLzz&ϴ"40S|{P@"l _Ӭg-?c/mHflf #=o1puW*O١z^(%X]J*uWܷ3S]+3WaFCFokGӋYI?dH~~;IӛNq1 l!qK[;}X*{[(kq _я&I07/8?,lN՛8 O80֫.7ֱT *mTr[ӷ~hsV H{VKYbO̷JهqI/6r ? {+wF2/z^EtJP@1B]&!Zx}ΦKA&Ke1/>$f[(lʽ2NΉGD Q/R2ic;c^ Ul]zz7D:u%z7 &nr-~tޢ%dVpӽiyudv]?IơKs^9+yO ZBQ 14宸8 "y iN'j ;e#0s!k}%pIo/дr. Ǟd<ǸlþMY漌wSv@⏱sk$ )ئ } .#p_a6<&1ݼ=0yC7ett1AgLwJ}\J`l aV;#ʑFsF74 xqAеL7]B*o;ѪR<5?Vtシ>)|qEOa)̛Y^~.$J-DYك @<&>stLBphn4.rW.j:J`r~ӫR_BWB$_2x}{C+{Bjd/ fN4u)\Du'g2\ޅŮ]/m[ٳ +!珎W?/#}L-D`6v%OF6;_ʫ Tr-YZ0N1Q4g MYgL7|CZZh뷡XůV֠W)(%bEŝAd0XK|"#WVyXj1SC+#l \'A<0XJb7M\ x6nuwF&:F/ԙUKN&aRu|2~+>ܧqq6cT5$a!#fu^\}qF]UgeڐS`zmd\Qf]^0h 9qO{ƪ;]k U]L)yEXBTI[V-T5D r㈏Z?_Eyp^+ˬh!3/t1KW&ĉe^>C3ߩP8Y^4q7n\Žn7`{;*;k x'2{S*@MDUa֤ )Jc|<(cld񝩣+|"+S8B8e"J+%R4bUILYh_n+ \y#%6?H1sJ״-ڣGFbr54MlγuiuX:f+o!j+K<)ңcv^*eMGO{ha%t[pm0#d h&XCh;(k QUU?wL[{]Rd"n+Ay/QMvz"A?r [U=%&^T(C+D'ZrD3UZDjHBc0VT}5#։pE'`  CGeHvS  X/tҐ $Qc4.% SoTZ)<X%?ڛO ;0ԵbW?6KaްŊ`W6# 0j/\G?HӮ)W56`!39wJu׳т~[Ao=Ϡs C g&{i6jWxDvUe4ǭ\L M JM+UMFY#;SE+V=oJVn6e@\2P@:% H9ҏb(?$E"[ f b5Go~LM>N=rr,Xƽ1;Whn9Bݐ@EӰƣC3ʽYeo]bh/^k⌡:L/\Dj#&ߩI(yq'Ԋ6](uZ]y ^oY+e:(HM/2(1Q`,xb˔ Zy,tZxAjI$; D˳ei^5f*0{+X rvl^yMhKuUkjҐv@ @@$CDF2,SWSmdzN%ƪZ% Rd/]SS 3KayvL Ut$>.ݡ^c# *5B N5?<{UtlPM`p(I]\-klYu .>OQ,S|5BYfc:O)=1e-]<+_2*fF8# \ˈ$$Nɿ?jg펟Az&dZ602]-&Hʪ|:72E|Ij.Di'QbEЦ1t.k&4?HB0 H96'Og>;1g^戒E)5$JAϊB6@)?0cjs4b1! ; SO%ЂuAz 187P.ƺvAAfdQ{f)vTM4xϋ&u)3gm= 8Ka#-xM8'kFuL ߊvq"]7̏/5ד[wު]fkޤ*„݈.\?hx<:UsSFb0Q-=Bht/+jP@W#I偉FFn |˂"z_i$or̵Qr`I_6UX`}?&gct}K$)xܹƚ!U>T-.ϓ04)HG+<`G2j ۶"Tfd`Q-7IiUt9˚j7bK:D^X^ltZ$#gߖר*͐^'wgJT Osbe+i t%aiz* 1o˰E ^r\ՉO-[s.zUFB҈:r~ZQɪॺ7s֠[X`pK: 6D|=Sg#o"^A@/I &'1 K."k(4lxiy0c̾ y/7 _oS_Rk0_\n\qx+"^J2Y,VH1h"9{:.X~+_=r:!plT3!ME OXΑ_[\p;8XAF8 nڝV0Ļ~/oʥ;ʑ#J,/'*@wD΄ oYL6$%}d81j8N9`B `.ˬJ>k@ ǂ *5vKZ:_􂵪Sδ\4X"t1)y%\B ʉxnz{_xk@C =jY~M}g%7_jF[jYd3má}JVUs>>@BpzACH>Q|y;h Ɗ h2K3e)”$[lּ&EᘭcI$ d " McSS|%@c?;Q~'GNMK[BQzMm V4'Rjor[X.a Իeȗ:VtCĭTE.$zU;ruڄSֆheEN=,\H ϵ,K=aMk_0)ffj6U\3ڙg[$Kl&:>wȵ8\J:N4WE y*I 0VќX JYf"̵ '&@ضk|竇 {<=pwiG%}nU82=zy8mf˙ѩe<$Q x騛4 C +^)GW}:8"l#lgjϟ3*LN*xPg5hu";;zhIzմt tU8G5 JҰzWkrS4//!TsKQB)>!\fo zX<:od.QLu|jXM,vʯ v'p]D~.,\O`SI܅rҫğz=Th4@<1>W"]]eWҜ /#,W" #-^/#HW IҚjBcXs!7Vݺ°k;Gۅ7|Mͬ5V}9<{2dcΗT'fX)hMPX ͥ-/=xUϦm:z.9ƜUFÛJZ>|ݻs+lֲٰjAE^,[f C/^CKҀ4jx I7Լ7oW~ϣuﰎBkʉ.$lQ~#|_N~XQ|kwPq.gg3@Zc>AN/cHg n竉Л]*G DhŲrǀOhƺ&Pi~J!!R]>1Y‚ܾ68icbڵ)XB&mz@5|/L_:ڗ?tE]EA(_tLa$MTcS~sʏHȢ *z!d1_/RJNkFa=&5x`'BLa 3rK-Hj𙉽 5= V7-3z5aיk#X`!9t,r5x(J\ =Ϻ tfL=:7ْ,w|ѓ2"w!kGe15ڇxͻĬ8 mfEe' J;bo1Nw8ITʰFj[`K$.T7Y4ya/TUQgh#$d %8]P/Ydт1 lE$|۹Zyȵ.CvǠGX _"BS $ؤ>q sv9ca!~[pÑO)!iv`ޏ/En&ғ7oq>?Zbݲ fz~A:2Bֱy"dQS|>C(M^{ z.%i|RX.1>_! obH]Je=9^^WGЊm 24Rz[|:1M4vhe{c;}I߀[f^67 UtMH6z#~wm~"dA3O@Q Ԫuu[Q~1Nn~wfe +sd8jn+Ic[:)aiLa«{z|fۓw$]hOd}-|lbm#?~Yw ' s[ɱ6ȩQ 뒳*X@t /ݕ3i <>'ԭrlJ_°){}$Lc5.̡؜ߖ:3o E/%_ deAOet pt1!B!ƌNlECU")uLPފ{Xh>آCQc 7؋nvD`h:Z(ʣ'Y90] kL⚂#NP MU _p@dV͍&?5!V%F xNm3 <]so܇\CQzMpNծz8^𺎁 v!F' 2AzUknzJ5LX[̽"uy);KcKu%))FhǾ*N wFg[iH=~-aCi~J[Ixe]BX+HqG?`Dd?Fc)tGU࡞8Ꮨ Sc!cb/@%*n/ZhG(l9I.{?>" `,qѴ5[L |%K[N)FGM]tMXC>6o؀Sg`3K f\˻Mfh4[^uz@F+3Vh{;+|LΙZIJN]0Tm'.nuassk҇F/ @RnkL6{΀pl ;ÆAYG˜ugk 1H 37&nh;v 2vz_gu՞X~~Yi4շqӃQxZ|1S{#a|2} ekya"yX[e,߲#BW`:<4H Nf:ʕNFKUvwd{,}:d{8i6ITDXK \LZ,w INųMM$¨ti UV2!)N6QJDzS2+*ev d=϶_2=eCH(TwBꪈ..FJc6}ć zlC|? H7-VԸ%خ3ɷ{Is* uB3q,ac,{m%YPΏmbAD`K{>=ZU'r z 4ޱo $o/mF5FA$,ԣ=?CZ9}r~Ub_z*/L[wP -+]lV GN qH>o4҄U<8fy Ep^je G_+9y~ %G1c X]W{w1$ u=Kow}m2`7kV@o4gƥMuxEYoN >H4UC()il *U4>sHL*Wq3bDATQX;3(1895c\-|Xޞ L0dJ=T1F5a`|'2Q}bA@6I曷S:6d0aҖMu*C+xMlm=ANǨ)IvȎ%?&@6_‰_D @(zLNWadz2G~~ qܛ=G11Y.c=xV˄ja1c 2 3 9vhtV5ͰS:fO1 QYyxD( cO$#@tĘneV%;/5 CsQQ6Xzrͩ0VJ ,Q0놴K"N`gޚ\ ߐ tl잉tR/BzV <ʹ/I;_{`MwI%wPj?)1>l WmRW€j_݀[@V_=za`^>͹7q[X\x0i GXkz\ɫR<cq3U6xiWY :z#:WZY<-=ެ,-rҞ*+~C FUvڨ EgԤ>iE-G}|D A3ƌr"ULeP|a =L؅|HUC Y!LAb8ˉ\ǨeeXN8X {c)XL] 6K<5oHM2rd !5oZ:/V(~Hes_=ݍaLLA5' VO[ڋ/eC3:-1\2UE[T{&LD=j  4dY ?`眿O^=% L0inG3VAlL-_Ē? tjbAQH\z@gYIKj,wʪ- 1Uh4\K91,Ǒ^PrGA@yIq؅Q;3P\A~/bY_m/ց2[| 󂇋` V<6T H/02+|?+ Y]#QR m:"(evBr߻(% P]_+UmE T!X>φƱ5lRSxJA*֫2ggϹ_1v6z yy$/MhNF:&p3>ע0_s詌 /yXo>oS*yuшq7ҪOr'HDN{l@Kk4:\Zpǰ R(9έkf$y'߭j+M=`{~iQsU1m3<̍ e!>#GJR)5dñwvB58#?x'y?pmF?o9Շo6jEBϝ"\iEC JU}2ULJ^/0vE_ƘeP̐Ĉl9G (^|O=wҟiDzNm/RI>Y=Χ\`G` R|iq d@+Fcߐy 9YtuzjW*(قE2IE@U/[ %ɡ ~?[Jӷ('ūwHFbH^*@upD/>-7#oIhO rN8't 9屠لΫQ뮐1͌شzkۢW+zKHjzx#".WQ|z+ǺuL@AsϿ:GY[5fbuj@Rd3>5Sy)K gO&WpS45݊Tco& S!ؿ|ْM>_ga-XuW9#bjLth|{N::N=H>UkUP>DGe_'ѵFl0˿/)Ҥp]?9)ޫ_\bp%6Ü EӪ\'Nc0ڋږj.v}%67=BR]]<3:H~эW9ފ4C{E\a ~,cC{݁4Ŕv{>e5c ݁y?E]vvSw {MB@[ }23g0[00!;Zbrdw#T2hVm,|'1ޒy+zI` T!ٵm(Z{uuVm:fdB2u'y"Wp -:UŎ]K))-Ї[-K@sG͢ 8%M)Dc z][#qc,1I>Hߺ(Q:ꁒyp.p.`n 6vK*άr%np sALvK,傡k4qQw;kA/Y tf/`ipKF´P9CZz:|ך9Wя-cM>{i+`h=jdA$sύ-K<],FEG<DGº=WÆ:&WUqbq-h*Y2k$-R,AI?f d]h|4 _}'PH[Zڶb(@\Q?f9}CZ-OD lN4GSerVV(e #9 A%k)xvlJanZa_Uhl͟e2p -o\b/mYO'H4頄x=kc#RbmʳVN;{rKQT  u/n6l$}S-!NEzt&!SZhop2ܘdlD PDKu+@:1fM\B'߀9I^*|# V'<#DNSGP8> ӌPf^;& öޡ N;[#GaM,lkZ  GjB>;AѲ[£Q3DlPZPHE mTBa$kҬ#P |]6͗2 &uU"K:;4ºa_c\4NsRjlZZڱQRm)90Amjxw[牌.![_P1->Pr@jbGDŽ(:t+繻><]/#7Y4l{ݙVu6gm: Z8$Y7nhxe_xH͞&(#վR*%=kی̌\z8FW+ߒGzKJܑXc^FG*腛\1OQ+lǜ?ñ폭gnO~ VM~%vkѶۨ+5QҫgV&\ڹk%wԏг0ou!N[7k9 P4h؉dG,ON|r|gرak_7|A31։g₠:ldK@I=ʕEVoռ?|"Gw/ N J{ <%Л7l;₏}= 5(d,W)BM 7db^;wGNq^f-7"Pů&$\Vz%[\r/"4&"ZY0+)yO`')wLr}R&$B%![K3X𗫲lq_FaHэ_?<ˍ?x'lX5.dڒ ]s PQ+!rڀ7ըkǛa2&Ǚ`LAsxOdvR>BzW ]rf0b6d<Z$iP Y`WMVS>g6 ,LF't D̹ݶA.IZ.Q7l^" !VD3}Ek#,{ (̸ܳ}/QɛX|6tuߧtfMgx}µxY!Or^^3 /?qH=/|.&`Hs5ֽvaKZ`ھr]{ueHYӍb<,d1Zd5BURfmq UxYQ+?Wu[>q IM5}Lٚ! 5nhAD~fëWHGض+Dvf(=9S2(S}.h:/u,W$TQYE1?QUҳf@ڨ~Pcr_N$e5?C3JUywiX{r;rtL+/ ,mqCfc֕~%%ۤz\ɠZ)m[q6.xY(pAv8J%%]}Ї0PjQ6q ¬NL=3I=\rv<~\?yސIQior Mrq56P"^J]7}%O9VOƗM2X7FZ}ݭPɥQT'VY٣3a"ȀwYN.GS l}65狕/]UE@I* &8*mB]̢ 9)VB?*jJc/EV"}y 9 fGI{ PS$A:pa#]"h)2Lc h /'gbIņzX J^V1W@۲M7ddmQFԗgvޕVg,Ek\IZmw&=L'ڍq͛48hkomT{zoLdeM*ȇji_s|1"]^Xh D*]@{\Y8-byj:`Y_.x,T^B)|и~$i=0O{;P<*U O[(m DX9AXCE8FN#7ypE" o uuz4~_yac;0{1!]5J q畲m5T19F$|a}`=F:~?X{`0] 3|kǟ lP,gD.FR4}<,/78Jgl0/jl}< 5PP{MWx} F[{QgkNz}"_~BO bV]bBln`C₃ґڍƪzxQ妅 w'"nnƌ~lKգO>2Z5+*sk>v@hŋ^ldk^߄ FM䅌mgX&ǵ_4mU_p &QCyY ޒ6&s0j 1 kn)D*} :jzR|"7-2;u.tv60Gi̘˔L%*+V'/vFu~/_΃J%@s8{Ǵ7;(iq逇y$7]<Вzض`SL@qI|i,;pLW̉h|͡,#u6 ֳYqw穀Y*,uEgcZVSjtP؀|}G)[g\V0:jGuǿi[+^ATfzF%4mc;֜t˒}R^%m" kXk~!r;a0:66FDhFK8̘ niqz)膭M[kC2 xWeƕq:̂ ˣZsyTʛA(bĂ7{UMz#0Up~!YiȼiL{HBSf^{yalD: ,V^!_DȘ% 74ƱnmSȬMSC^~gۛT/G IjGI#6d$%@#f@\6?]&zT5gQ:|fjqrqߒbTa.hW3E݌JqhD΁_uoZľZL"*`}`DU4J UK,}KɀSw1ҍp`3]k wt&Ěv@^*ݝ?P# TA(3Ic)+Oc?୸NnN"_ #M7E7C 8ٍ OO(X('r]`*ŏF"3WL#6gC‚*X32CY)4fUɺxտ޲AgN 8q}G3*9ڲgިЉ)exO]n;AVCjYna~\b"ʸ9rFV.Ɲ%W4ylyFOvc(T".7%zq~P"q? )P ̛%<@#;7(!L`gigiD"$f]ZebvlYRDb^*v]WW@gyR_t v@~rdd0ukϿ}E#U^#UznʹUNWFSG,]G*( F;ZV,ۿ%*_C\~<>$8wVֲ#MЈG6=2.Ywʇڸ+Jy3Bg% D;-/LVq 93od.Kxp,_.ힳz68@[ΘhVyGҴNw!`Њ`1*O9jL }4&ϴ[k6]2U _!g+eF;B*нኰm=:pӉ1睅hy<> hМTysLNgC}?<5"E.S%#;%N}׫Yb炱P:\o虯j@د@J`\K?k/O N[g\,Ƚmp=eQ ŦUQ(o8_/Tf.ko6\HLfok f lt#ljGu.窐aR.K5N":$a1LF>-B1vσ|ja4 6 5Om`B:'U(m H[662{st Sݖ-Yq8gҰX2z[% qz֜z N}lS\:1t :% qԪ$&JsmU.*,PukJd1BWkfg*aq:#$_. zpa2kU>r|氦1u >/E=eI*`p1 |` ^:۱튗Txa>a3kj O|h+d95>·wr=&/Ol.b6an.(ޱd'46TP Xv2yItFbn,B%FDž2rGj S3 ϧ6P/ѬlMgȫ1~CH;" )K`R8.EOo@qf!&Q[D\@WZ[:CH.)c$5.B,ɶr%($UF X-Б,*Ltj1I{uzـ~"06l3+׼2%Y dA (9W3=yX͕8T>v$(R.BJA#`^,8Eܹ.׏1[yڇfsXy7ѐNqX/su TrU"o&M/t9d3  d+Ft]K"$-]7NOuNyaG0B-'@ծ0 U_KnT4ٕ :Α5q̿LIq庣xĐTXS5aҪ7G`T=G+? /b3>fXVbpF N]h>˂6o㩶gw`]um6J_ r68hL9a;w*! R8?m*H'z؎ǐc@[8oRzDEU6(m^ϐ: jz\E D|P@Ɯ"Ŷ!o #u65;4SjC%\iQV. ^ HZQG+$u<kCwpn 9EN^QV']FaaM[؄UwKoDVsÏW'P.pP%KJ?gvtaۗɇKO&,N"L 9n4t)?LVAޗ: >[3v؛iǴ*,P:wnuG5dH,j:il*u O{lRQZVwgyxrGfIrD<(nƈWԛfQjè-s.@هNw#܅eǶb2zIR5Li{YtGҒy(/x,{&oy#:(U;)%>}?6ѧm4/Iu>l $" dhᆕ3.@U{PG\"wîYӢ |PFOn:P 3m~lHF~O*Ps_j6ǻ(-zqjQabl[Z9 X`xɦ72K\U옸+t5#8R.ZXpJ5L)<Ѻ5:gM|0LJh j[ hhS~s4C;r0æ;1Հ>b!fXXA 2`DX*x^Y-%%_Z"yjW8N@4zÁat_j3~0ފt#$Dg6L>kR@.}+)vL6oaGBRa4''(SW;3Za2h>~ J%DˎwߟW x³=ԙGb8"T`W SYä #Ӿx:pp7 :p+Wiޑ.c|'@ݞdYJΜA\Ԍώ2L C(EIlKIO$ #A3%j1+o39 ýn_:;$ƚ>n Z28 Уi 0AM11&dMâXDC, Õō"I?WY3I w7rfퟹGI]egp73z ;1 H# :ms?jJ0iB!z/qءjy;Z2^@ѣFE|dN}!v1IGFR-s Ub:iD2YELʳݳiCρJƧ@8TP{R >?rl$:sUD#͝5L,:a;bk| Fwj':7L{p/$N.}X{fW^$ݜ9cF,R^H4[4o]* { Y!6ec!q6kj"U8%-A鑰igt0K i12O( %'O̯DH[j:(:g/F*H0ߙN Z),k|V4[[fnmpwC:\A `MIϘPዡ^i51q?5 Yjo znxHkfM u0$77 n'+!ֈvc$v%?;b4ԜqR>,rfYmirؽREMe Dդ͢s3sbcٿCZ IǑΐV*&a.1d JH8zBv׸@6J%dK*2'!x"iOZa]ύݒez{A(6=X:U&p( hOx=/^FN_IpP[cu} ^6FC`\1,&;!}G pf {jdG{4<w|NX9㛻F/)Bm{\(K/_lE[JToIvyh@ ^B,[t0XVTߎ`O5|54 RdV\0Zn(*nBPLl , w%Eї310*5_݌4+8ڻO"vؖp-zRptYlfO.#Q& eS?JM2Ns.Ö-?6" ZBs(:>QTXJdz&Wg*)Њj``vFD얇ri_17_j]xdVr? [ V@YG8u2*NğW4Z?R!jP8QX:oZČ! RbػT=2ů||AjDqK2`!*M/D8 y6ފwޱ;HX̤ͯb)<;Ig`W+_e3Rj}6-2a #E~߰ ׋53n 2sʼnh땆 #%0Ś/ѐ( Br ]"vq{d:]ڠKaRŽt[>IW*â&)i}*AwȤ>زo3! 6"w7"^7SB dCWW-O |ݥDoFQ_DHjM>d+^Ր̱E1h>Ƴ_q%yc ;:k}"=vDzU5)/uq8ͩw뇂Y< y(n* gLŢOtu+UQk&FٓgП8 KUgrC>T$'%Rک_:amE$] 3=UF"]w-B-ޛd^V%|<9:i ܅TYf,̺P$gc|W0ʆ8[\$vb+tb CH_Djɼ)shUHc@ǑbmZsigZ˟ w1N nK&F DKu H<|šk@imF+/_8X5VlV#6*o]nW)[HPk} MʆgK@ѬAȕԌSеYeX$y%8ZJp$5CV㍐1U;&w_xK)sk ÷uDcMfRP/ќf_0Elp{6B~wlB\4-dPϨ"sMa+ҹ:a޿*x(6TΝh8+3+]ѤIrBȪB3'[Jhcn=0S~iS@JvPz>u RDʚ  efWl~+&}DG| ,U9$6#I ֏ܯedĜ' tOKJ(b&gPSo-N;fUAc&HU.{9tڂ{F&ϰ"w, a2`{ܔ)pEuL|{]+փ2V 8/w1]oV3'Î[eovyk)+Β_ɎX-L!j)ns5̸b4R7+AoQ?AV+J=tcA$iЭ@k 0 rBVuF`Qس]Ϳ\e/}NYsa*+H5BŠkҎedJۖ/P6+y!kf_S6 " Ďˤ9쫝55Zg&ϱ:"Ug+IJ jOmcɬk2? {^ 6P|8['`o؊2EAuRɚLW^r1*3暋^tqVtxf<ҶLGyِ=J]udb鹵ϗ?w%OaA[.-.W2䕂Sv'׏@\J?PO4첓^,6t?./8s ;lSmHsf[R_Fg6_OX3Wjֱ*Nu?□j {6\b f!j -]v=MAHs.zC,7۳_*-q ԉ Ev@+_RʹkMoU}(1;&t8 :t^)Hm@~v@E&/9}@ :g,*SIbk'VqUt>"PQщ* q`Ag_үЭ [yA+7JW4$ tBRҊCUb<&J2WG4\ڵS0O(;Oo&7^($\mpURi14NkkYF$jLSGyZu| H`!M0H5jti7XM nzL\]TxJvX>(#vx0b|z9d_&˫_TJ m3R`=4˯Ȥu jPJ}_)_mkkgГ(h!Vw2hVYTs&~ Q'{svVyG#NϦ\%;?2w\ߐ*)\98 0?L>5׀X\!)nB-sWo{YN9D*7 ,L7[rҟL|Iqv/do[>jaː'ETڦvmB3עZtө0 hQgצi0;sSts{v{ GysdFtarbdEPT:u4Dq}js~j]Zʎ蓄 6#[= RCCWZ▕2m*\4ؕbvCw E%h;P7S? 6S..l1{󔽭|ϾZe@yB4TJ3[!v 8JFPgR±ԁHxfex< ~=rk鏼>~sc0ƅ?NdK 8n+? Gs)o{nss1W}J" 2}NMKk\IVge.@ӷ1+M-(u%-2D@nNRԶSv=XNz`*zF ӼO7^@{gΕ: IiyUԂ ,nz1DEDh]d*6v Oz\ {#G0 QsvI}&Y\ysјi?E/:bWR5FBcu.t/#a+9$3 ^pr9ȣB\8 yߍOO2#Za#U1 h]P.c÷Ӛ7Kh~̛hP'P(>Ш7: av 瘺n ^Ԫ⍕ 3W՞eجwz4#mau@G:FeQČwĬy, Ș*yy]Ph:l NZ]^OϜ)˃ka)V2lt1K `ړhx4P_fNP8.tMkLm>IFNâ9}Y(f)SH0ܹY$vF[>d.6򂇺c0ۤLS>!JXVܟ"hgv*Ev%H7K8Vi)(TR0?qÈp\8zkL6 ,:*\T?m1Қew=X2q$f m8L*ﺱ yؑ?^=i!|90TjdTС Á5 Pnmw _ =.NY3Tlc5zq6?LߌktJq1C~,ԩDX;E! "5^lt k~JDWvEQZKP_Eqc˲r#%VOd' f'Ko+pH~kHB} @v霄f*Φ~b&7l`>G2/[.}=myTD% ֥3r%ŋ!֢;v ;7#U@ºU %}X{ TWPϤ]m@XUj(bk`l޷08?gb\1`AGX3ܾ֭]^-2wCˮ" `=bC:8)?yO4&RJI?H3u]ۻ-~w F~W~e2[Nki\ GV\]VoZ| uV}D>Dw|.t#1N1=R*".oW]-Y;:SQ 1 { YCކkkULp 2#JQUQ1ЙxωWѝw$;9$Twx)thIf !BefaU3c!Z-/ $D@B! ٜRIQ4ћWpL J|PVaH5&3F0)mAc$:?Q_)<Ō8;VT.rzRJ<>HQ)Cb;]c G p湒l.*(m}|ƁʬֆE+sH O`ڿeLۊn7:oGiJ1%v׌9LrݡZ[rr"ȘE!uWJn"(CD' X'j$|~h$0&EP+f#r/{{?X9'rڅ4@↜7s~hZRncebǔHׂF"[u&w_#Kse\Tjzh Q'/]b7|mR=Ekj K/V,%v/7a1 1n$"LO\8 ClpAruRk}O©d0D, &^jļ0z-9/&Ee3.FGYFJބ y he1m*g"W㛀^k$6d> 92ɱwԈʈV'E>{#4M߇?zyVJIq,:c/E՘AUVjHƹn*Y,VN RNBۂkY{9ø #bunxp N۩+y9\uozq*!CO"&ܛe&9ZDI]wmiO \[ü{Kʊm+O\ ,0 KiNc0Ea9iO`EA=-ܚBh6> u|R.з5LoiCl/Sy6xȨȁcz|oXvb H, >EtfߞN"[ ?ƕ>Qj=jq"#B |:Ֆl*;g6>I u#X NύB0~OPq`z2H]8j$h|-r6*'5VR gwT3YV܇牝[8`B8q<%FF%(QNș؟[\ܦ'Ȃn! HHndP[ cLRZU*v`Eb]P j;rp*F.e4ޘ[/Tvl2hTY3[뢧~r`NKàq';~zJ랱~QU϶Οtk pjf0%f7IoBs?2w5N<%$O{`-ndHZJA+?H }\#c_S4p΂ YHijlDB(cQo'=)2ߙVy1Adg"gF FeZ'#7Sz'ڬ77혡 W ;jmZB_+\72~D`+ 0 8tRC`Ww3"Alou=/Ƙ0h$qr#ѡ/N]=Ph%BvF&ȥHbD+.X%hXڊ>1.+l6c3N'6NPK#B$g.w& &<ᜎ70YKv5`T O?-Xj!]PYP02n}(>n+;Un " p.?XFN##]*m!ؗ{<6YYRỞP¸Fq FRSXQRD>EcWO{Z?yQRNLYnXM@Z1Aܛsgy/`kڕXJmN ]8~p{tN?ҏ~~Qx¦S2G@jJB2 4Mä I٭M$1~J:}Vy ~)mw^TU[VXApgk{lRXY{"!~(nNֻґ\橻@aY8kZgEҩuT:x 𓼯p5ʓTUM)b& _b ]$ǂXɋ<`+ϿnA<}+..aw,0* lVETVK=WJ^ՇH$,Fcs`&?^ 8nuGSI5Jӄ,$/~|zjV rŕa],C4_| YYY`1˽gjd@%ZvYΫ @9@ ?!v^UJQ)f_s pēb>d9hښ lgaҒnWzy\xO(cbzaem-9dInbRuFƲ\qwvq95aP֞#C?eK]eC` ^:£<ظb!v{(wHS[UZZׯCK`3R]Ddc2T_ʃ`ȫ ²P GƄnB2(߄ʺ+HQ+)w 1  XC32JaּÕC¯~mOQ៮~o2gpv9j4Ȯ͆m m뀨hKz oDͺbv/#f+鲞s}GB5wU2{{[#͞@;rnkuEV6*9Hf/g(^eeC/erkC57|3樱jZtkϸٟ_zȝ|$pW|c&gD L \zdltwyeET Q(p\^摞zfc73&~ ohTj{CھU_unM!S3(gvK2TTw]r?Թe*E,GOHCpX}쀼vz\O&p%*,9ej|.Q$P#PK}KCA^@_ylP0g9ڈ?1;5לoe!2KYk)stxA%0J!qAjO #UG헓"ةEwhmFV%݄z)g>QiJjzf9l3`EU%F} /R,G\/U͐bcA21ʭq.z,? .٤1(fCJD)0]`7#dQ$u_Vd((n?в6&LEzaZC \eXXԉ- -El-s%fqt{e9FH8%^1ZwOI~T S>+X.[_; ÒQ#yp*(^v呣Т^A6)> `оdq5&FN8A >Vg^ jgeɜڑU퍡qzf'9> g%ufk"Qe/ikʼn up!JQg#"~ λd jGwư$`Wσ+ZF#x? jl[5*45y $ |X""Y_\Lp;g[whuգ_1S,SČ׈?cI&jAe&zGi6lg ՉW3[P SМ2p]IYHf4@>gY 2jk=-Lm'Qg,_쁙{>ÝT49|KOvLĀ▸>4yenU&^u™0Uzda*!1bFo_4^ƒ' \cbl"g l_݄qlol:p04|B`@du+Ϙ?zA&c3IX&;vM-l̯(aTo(9kZ˺h!hI$Nvyvs9saG3V<k(^_>l !yoɿ4CaDŽ|(CW&7u/<d{JAPQh+p B%6Hwڧ\M+ᝁhx=n?EfdsTy#LAAiz''~!S&r+mCr.p+6`\ r4@ÆTYV{ _,4]^9"0\E \+p#ʨ9X/(ێ>+j{喆`'G6tڍ. y |=8B{QrQ[-pANJąU[8f]¶ E}D;ϾW>hj* FZ:plaQҖ)ט"yU۰ x\p"f?ǔ-:Q>@_Z k)xi&D]J=)8߇2%@D-%QaI9!0Bq+tI>DXN8) [;*)_Lf34ab<^| ThYbO_hI]# U5 {Tg:|bB%+CֻW^X`8o;pj9lpuwN'=G|nht2O1J i^ʬ#/*aUw4žxhJV]h%KXI?+Z}p^$0Pp 8l%iMlcIw<xęA]#eܰʧYҠ]Y e͚Kph_Mwܛu:f"!̜:,n`k-#P)?l?kn~2ڹ^ۃE .){a(M۰(fc> TmE0)$XRXSn ok0n>EAjw!vb^c 7JS#_=ZJucvlka8(Wt>SZĵQ_m]" f<`1x^؀iryIHE3tn޸.@;IbcP]C vDZ 9wpwt i~g_&83?r}LLH $(4{tL!8LcM20RoYJHܒsdX4QK:˧? JGR0; zt?kēaӯTFD@LW/6l% ֧c!.-N ]eWppaV[ X8#GY4B"[9%f^ͽ2ʰf0&e"~pV8uTg%̜Q;8i4$:/KyUү|1uvƸss,`T  =E܃ ;yiiLlgCyQsYpd(}.">qPV]鰞W^0bHOS\颣>1VUITruO8.2$py%.z}bcrȥ0ve:ZV7R{`$du 9X:z/U$b(4 dd,xȜ#~0P~6SE.S$ ]qDR 7ZB7CvI(/)lLo /ɊYд@M%nZc* jߘ=^US)d"S0ȏ'WY9^L9txC@,|f%R>e AFkAjYKRMK&ו\l| O"u0z~UpL͏ FG/~>8|iclRͭGjO` p`cj&pS¢tëYiPB d@+(4enߊLjtiv)K~oq^A*]'jlw9NB'hhIi%^d~qגTfRs}΍2=Cll|8YV'~.axq#$T):@8u:bB:vy*<( 1u#mϋ0}ZnՌ^_SRBY%>*f L',Y[2`uh_YL}* VƺPn%srq+2}䕴 wf*?kJS]4CB 9R"Ņ9E%٧ O~GEd)DQobr/pRA̮KN?G'Jē&a @VFm+Z1/Κ"܎pE+@q\`DӮ>WxU vۘ Hb#.v/("wl@ &^{}4%rUMJ բbzW-9CBAV`u|f>0?C[ϑ0Fp;3adQh P1t\BHuKyڟicr"aMs@/ ?Mϡ%ԢK2"EP yꛝ} r ǻE5'z^Xx>A]!O-,IpŅkc"`-}C#R6m tEעDOUU9 T/Rk]?ˡIJxd7 7 \=iS12z>3\\Nb~pVQco+S}IEػ8O_EK] gXs/8&az? .-Yo ¡1fOڨ{\Z$3:gx?1h.ҏ4ijάb7U_I'4p*], '帯V2 $#_xokĂ.TͲNT}@IhpyS!߬^ih$=:s5w`쑘աi T~p8  Izhj7.'ti vՈҧ&#kqI-S2Zf37 @}n]Q=!?0rKe-zc+.[?pLW ~C .cWLsB`$+B%L Kx[K$Ksc<|afG X `։m\lk!a,O K22Ib$Kcx`r&r:rM.\􅷨ЃY'mHTrM1[)AzI54$d_RQ»Ȇ6a+kQ \SNgX k|YQTq!Z|:|U3g1,(Ґ!J۵~rD*LzHWށq"s*2>Hl0"YQ4bOF}N>I&5\U=kb=lDOQJhZb) 'OZZ(X+RP&!#~-?nل~|@g߃')@̘a"QNY3 AKF`,$8Paݱ\cTE hUy":38a&< 8r#XTڋb6)13F&U(LloZ ×LGoA^ƗK^ % !(\ov.S+ލH갊ՕK yo% DH"R?<,~phnXY_{A#T 攎ZMs'Nn0VT<)jh)$2&N6Oi}܄MV0E FJSl+ҵ[_[^,-3R*cZNp"<夅1e1>zH*GMwMdjE[T<pO'[qr=fS ͞z˱N~1秝;C<1Q~- Kb67WaAbL#n~guIc+ANU(!4B;#Y)fo eueW#Nk+nxBTa6ۢh{vY $z{UV^Goz geRXȈҤ%ܡSTLȟ+,B$Nw) 3\t]y&j ] |50(t`%9gF벢vT'/el.N'\f/"MoEa(X=MhcO,z[xՌ㨤 F ed=,2ӂ-kqk5={e% C S+H'p+kk$+vo_Y;;`aNIҽ1U޴cc h.p:Z;aΈH8BduBֆyyAX۴ :wY'^w49@A>BSE_s5]"N%D<))zvֻ mmN ?-X| ùҊT `-x og'%,^ [k,X/ϨaZdU@eN.v0s"mҙ  8ԯMU!\ڙhIZeA:5@ĉ$˂b^V^cX;R:RjtFWOtO_}(+kʎ_`}NbTE4[9Ь(."~by:2{Q[fRY3] \P)a=`;qjj|דjSQD/.++Yx8uIxTXp`z4Z62"). ^H낝Xd,R=s 判/g5K6iBK([>j&j4HmaM4uf6gꝄaLPaoO4vneRl;T'X׎i. [%3=X6J[TR}5Y@B Q _;9޹PTlY䮹ҥKIbXy%:($Y'[Z(*ݞ}"p)k?Y1 R턡GI8E(K@˧x1aI.6J1{@%&{|`o[oA7)p#Z7 I{b2DgCRjtN.~MI{RgS1͜pWDz|$>k`^" [L VjUPR^eJ&v+:xG.L?o*ʢ QYvTWOvؠ|(~S#/S<B/PVdȹc|*| d_ܣ] $P رalN/ *=ui3ؑ:KIͶ |wH/k6H¶h?ؙWhzzlv++kA*sWFbq`[5O29E]/,I-Iv/PmA[Lbu*SH޺ͬZZ({A+^ ӹsY8u֐zVzHa 2+V !e-LF}h[??x+SO/ČbE~a84pݙ+9Ź) )AL>|-'j.VL"~4^i<U[S]̧_1w58T ~ n=.5Y:EZ++WP58̓V4|7c:@!@թR|Ï|*R=,8vvE.hZd:P6$ @anBU; vC؎詒7gI\ n2a1\zu} WgE.:[R(ɚ6 hZ[e71ޚE4nv FdGqђD~OD ϔ*({H|pҷаL'vc d맠tK, Xh(5uP!*tж(M-!xc볉 R&(UK.=z3ϑ#=uRg$9,4EBR*mm;)'34O%LP 4L0 = D:I`Fd ]gXp@]ó adkS:MCwˍA]aɅz]+»ÑKԻeN{e~*H/BP J~G>{0O >Wɹ &T"XO}=jHтVئ&Ee r< Oʔ_;hW}xU`:E*'7"ڠZڿ7x@Ή4{hZHIu,bWXG E\oJ2Ϭp'.Tm#uݠ c(նBI?MɑnfLnSic@j :IfW8H<<{~:PHDF}lGս%IWa2PD_{jTBs:IC Boy,9~GNSpd][L meILhⵓ&9v99"Yg`=Xs͒}?сvn۱N,3 +2B`>FjpfW3Oq>ܜ1#I5%M<7WB`34{lz rNZ) x?o"i7plXB0qxA3SP)hFmIwHp|QzNݖJuK@ l:\lõ U_WǢj:lga;4y>uEэ0lzbB\vu+$4KC2܊2D<"ztP2 ty9{ ");9ӯ3!+UU N;?\DҋYT p+ #|tu%X[ի:$|#lG~6! !|ݒA&,fA 2j8HQ}s@+Y1>;zHTS +hF<(eQ<sITR2pDO>2|D7$kNX0``@;̐/4lKl\|:\,`eom9N9"L7 d« o"w^lF{% bAP Ho.YHYw .eT5U\aTܼˠnûU64)_Ӂ9~{<إתӿiev{,ݾ!l6dtygS/A*־8"oBU.IAiWNdcW6AI=#z^hUAx[Kз tL{7VZnO̚6';¡jptWķVT1rHi̗,/|V18Sj:=f >Ί щgO$Ō&͈kWm|/gu 1~Mp_vˌ gy]&kSL755L%7GM<+*Q⵾陃c|+#efOAn_5I%c^~戔n>A(꤬. :ſ&MOـa4Tި! PzڝEBo+6]3`MIU;{CXx׌%>m=뱍)1"AKGuޮ0C4=ϴtS'@|Zٜ:vdnS赺n7–ipYr\l=sL,4SWyH @ wDIԈQ!ZBqʃAVwc(bBOx名D/طJMN4.=˾VP)^(PMpp5Ȩsݍځn9*mƲsyG eH(Iz~~4NDRC6R'q&41hPF\{pY%?IpdzXҤXA+;Oj$1FG'6sqqL}YP0]h?v[dR?Ulb~s``?*82WLthT#!hDa։)EЁHҭ]%6+ KIw r'f'C*Pij0~O㾼^bp"ŊT[mEf%Zv˵_k6k 8)~wD5&Sd:ۀl(j0$:@Yg! A;HTVS7Dw5Iyz%A_^+%m+sMr :̻K @7SYagi5|oPSvitD*Gj7{tm!SȶP]c :$f"{;}IJ3.L7.aY9/lW=^͂_V`2N/ I8cA\+7D)4N|0j/|3Ygs-VeVq3 ]u^~cr;wSc3̿ h3)ew7 %^gL)8OXQE8ѶtdW_*j!@w_)+Y6ݟ)>e,R up pٔ6 Cr"Yo?⒗_QޠDu}[nج,"Scyny%3 rvzMW~oW$|٬}.g!83_W/J/a.ڐW>]a˃Xw1-aҸkל8GP 7\<92[["bb)z ;,DxyocMjaj2(_ 8`[[oapy3;FegppϬ1HaH,g3QO -Iy]@68F !hxmi8<>؉S YMzY$!w#~;`'WRo>H\*.EME;DYSk@S[I~!4GvM^H_V!4wpL:-dqX*J%7)?eB8 @YZV!?>uٵH\@&Eߑpcѫ^jpWvhyL%BWZ{4^Yx6;zoA>qu@PO-)rBx$GKݷIf1 7 $ <]!2܇xY5 %F2uyX@>WۨCe> %$[؉W=Ś01qfw cap@R6JQ/}d?p ܁aqݭc% c^H蜑k3e*d= v<+;ϧjΘjp y8rXBy,"J*`DmUf=W͑1Fd .kTìxR;G1G1o{ S;LgkOe,*[k(7Cѥ&r&H)# קy)_/Q.O |.YhXF}97 )cx"2`? sR@Jiy?M=q_ch=UWgfqnێ_wQ[7Ef %ۖ3Y e8)VҷcP xv~`tET' tFٳ[oWX?$S#~^! `cx$GƬOmwrbq@(;"\hgWFI2cZec*%CxnUݑKf1 @ +螛5 08 cNV-_h$xN8ϟԝAmD*#.=ҍhMO"fu#{ ;h\_9'Z_EםKnM|[D>Ƿ!dk0Eԋ%1 !'3lAшC_Ou97B6/ a蹥nVKӪt ]x~>3h#fM 7K|r ;å(i$R.n.v(m7Տ5ƝQ[uUs`v4ϩ(==@3X%9_Sg%'+`|DnKt='~&/`bo;,fK\O+!0EDD_i@kz!d=F(elYzIL1_G*sMXE܃Ow'OiכPib@JgKyjZg_!G?vhq-b@}Kn+5Y'V Fm8NܨSܗmr0DLms6ݶ`8!}6R G)Y~CDGΔ[@=sh eO✚ï 譜Ϻm>ljpڿ,/ջ RntWf~L1ݵ‹Εu:3gPP<?v*9K)ϡ9ߨaiIFu^UȪZ~|!W8C{q>^ GRUd\)&5ЖMmctrjm#]NއᛣYIt5o!E(tF`Z IgdΘjiD-#.sVgշ6*b6F;>"? v%4WX³_h:60HkCi ɉ ±IO|/o;yުw }GǸ=l(s%O-i9rMW%zO@+)\շ%TL&-hY禦2K(3j:,-vA›tB\cWb3v\(>!'- blK Y]!X85?1M4cJBXѓw>=3܀PfISklo${9vkڣ v3Aɾ,SMT_~/}AᴣQx^D .4Nmw:9XJߋ/WP<(G-8Jc_au?Je@eQ,#i)a+$"CKO˵G|/0hS2GcO|9w N6 aleɹqdѡSf3_4}i#*v/MW礞x$i\ևgȣ=4 f-3t3Pr L8aZzXL :0),"}͔)P9ǷL/ 'wBogV)XdLDtsi[ s&6"=:$!T 4?M).^:S%jYs_hLQh"Pt:`?3܈1d .@Eyk6xF1m Pog,8}GPfLm 8LF>Ͼĺ҅ج3sYD7X$5RJ+=㈒v3y jW1I쫨e&^s2~t.8<[Z#Jm&;;m:^QQ{c6=ܛ5}Lh~GA~ƪamV: Kfa8NPXujUyBv-vp~ndaqlwUiw1P~ \L I|Hs[$yYӋYް_@"T5Q)ivBA>AMqƍ!p$|O3O!9$}ΡpsFZ6q'䳠@ZC "i{~0.e?jP(}E1/~1 ;>>0M΢>G-BsY2HG wz%N2cBxLM0nC1e0=Y6ȸ=i?洚&Y1&  *5p(s)#5~, T7uGi3:5ni+cKia=ST>'~Ӻl: ЮKlT*費_GÊYzO.n@gJD,:ƥ9BZP%sJlw+:J%K2U.JVݙ&V}e{Zi.Լ m#i" `7A- aeD]'2wLa#*6>v>NӴ2 @ Rk=V,㧺Sl+ǭx[YuR[IRBh]$+ϙ5e*bU5̏ Ilo, "R 9{EM`ko\~gh&s׉+ PQ"/mO^^_jAEU.!V<6 Opc;g&(6yi`% Ydug&] ^XQ\ Y'3tGjK1O|NcM{I%p#tq![O출UA#w+u¸״{djѥ ?DDzslia)mL8 T3) OO_+]ؘe3'dxx[b dݲ}یa{^oR׋&bxjNn}glz|TX(b_ofIakAmB昧/sc?|Q8ZTḀ6~Ako tLt| SlP#ִe`խ\p1nD֜=-]w{-|_9)o& vT:萋wF&újql ۖ02kdӽN,l9m4H/pƠ-kVJNb YXUC7@%˖6>:hXf{Q> w鐫  G|h-P_pʘ˵}HV2˞w"ULF4+#8kpm4x0 Dnɛ Z)Q ֝ݲ* yRj\ʰ^ Z Ľ%֓j:]!*` PD =E+m.W rڻ\ow<8wp2u96kno[= I$q|i7_onL.Pq |Ji6\](xd|@akۙ-ߗHA~UW =#,Z+;#@WS&#7 'b(Ҡy.C$i|6Y Hη.@SE8{_Іl- 6x"9>йqm.Q*}+Z/=x$,m{=yoK}ϕsf8ĥO"/ ޸`}h˱BgoUַ vkҫcX#Ow82.$YO€4٥>Hʌz6ch#7+Me%icF{w-M@(A`UZ,ր,X̀ߔx5s0JjQ*z$|ߜdc:&"L+q^,c7ͶEcr-M;)bAflC3δ["&R{+FazĹ4dwtY;m7ғ,7}zmA)$6lؐNk?.5̊}0TbI&- se#SG'E*bٟ JU.UgdlbD3QmM9/j"4{j=Mn&7w #ΩL-5o S4T"|N?DC?~ Hu65yHrTg\v8󥱖P<,ʊMX$m, fXT؞Q%őd*a4F|AtPHgYqo5}'+HϽ:nAY;񣪕UKD./?T4 4}m%~EhąI&ˡX&0;WIaF4m7=]5qľT`k`$Lg-8v9b-n %P=R:l1Bs=駊M낦m,?;V RhiPI$LiŸyE !x>9 *Z"@GyavLm;9>t!#څ4#]^"ꊧy'㷗 Ǹy(L{'3VS /A=KZ@CP^!mK!* .k܅߰pqNptY 0玴TxiJxwP75 L/X]7/ (<\ϷYIM?? %BR k蘁`NMNOEnY=kf`GE\}{ؓ(aBlEW޾S0"!`bSIlc>{Y{Rjm ?g&,ў&9?35$oMnSzPfV"+&e<KiRJ9 J%,čte Mzǜ*QX6]mg$]|V}zBeC5\,*%fU uܩ趠 e_'6Isk,HS^/f'.\eT  čjK0:v{tąA26`f퇚rJ_ʭ?!Ҍ$Wo^! oKk8||&`6ٿg.u|m>rQu)/h ڮ]PEEo'VOO]4i*vU2|c_iVŰDb ^;HDC[G'[H)ə_s;1?hH_(Dg50(P$ȺFl]!ȾQ6$Xus&vMyBڝL5}ĭHp +M #7zµ(5[J= F.CҖaV=`{cS^w~|J_AA+,r%۹4;` 't E/b̾9|u+'ɖ ;LeXY$.'_'{8|їt{!ǥj".b aal.Ι:$ǟaXvbt_p9{ k5D1' yZ-uL8xWNGk=ZgDȓW>?y,_Os_axۡig\jc5gzFy$4ZTT6b 2Pj>vo| 6Lf:ǣ0"UWiLo4OmEjdqO }V`'x!2V#xI!N;S"C:PRdB2=# j"+{Jדb/fqxd2K!ܿ[WnW2,]Th8{/JȢ?flPwEU#Y Yc Y`nJeKs#a2o@}ek6"wc@,%@6 P 9k`eEG̘xZ!FqyrJIpI=<`P]?_;kxyV )Mȋp&}n *LrLaUi$Zj?< ݩ\Oc2 8<7YV) ~d\C=,J}FҦJ@_?AڰeRAσVe.upLk | H̤Rw`r8>IzE^>3iWtRr[U@kOla1nxľ|S=*„*%U@Q Zt'x>(Z.@G̔ ;WQ࿪DC[Fմ=R:Gvy< uvH ȇT)I'7ׂY^"#X_9΋93?貯&_Hg&wDq~Y@wPB!"򔊟BL,pm~wzb|ltFxRuLX飕pĮn]`=ȦMN 1=В4Of3adYtorjL4aM='?&ľx HY{-,i45'Dž/XL)2>צ*͢fGj!yEzƏ8ujyG2<"+ve_س{7.jiK|o!%̔:]7\Ddlomڳݦ6Q'%Z禊}ZJ&t&0˸tNPnb6R_hHy:r/;R0_ D?%lmѠkKD5\P74dys#-pq--l$.wyGiAt8:nN31f&'^e* ]8]璦BnY?сk)D(r,ʫh;|}}/DfNO b8!}+d2eLBq9*@XoϺ{L܇b;S=NG(schR|#${q"Tʄ >Ȅ }}.-×4x7iRҳkD7Fؘ$piV4GDB,7wmV46M,IYk7fa*U2 tDsJKܖ=! YG 1"w,I('j{jŒD6o &bسDg͏)V4+P͡ 5rSub,ؿSTr$<ユ+420ΖCUǏK24q[]N{NU&nArΩ*AgR *& "wqe,K;/JHYT3&+i3c^WL?h$v@jzAԢ&v<<ⳙ<&! Qnv\1w%[ƿ5y0wXc"| pӐ}io ܳ-0kS3ہ+)bR^{aE rHv=,Bx*Vh1RV *p,yٹadw]8 Mb%v{vQBGe4'?gl^7uj|"I TP^P;^0ߩh}3&}nA ^a )kN$q&GN^f1v\SDPNzh3C~+b>ݻ9#<? J@Wvd&5(4yߵ7&BNU+*c$Q% 6!R-n$?`m| [ϡj3;3U>j ދ) dq&5Cnp|7m#1P$; Д W<8Q5G3g݊y+{eޅc|L2!=< /vo뛔!$APXXA+H54]lc8i5EOR$=+Ⱦ;=u6W1G2#ޟ cXܭGƜYy%Uf X@v ȉ~_X e'4PR^͜u}/劍lDq{JbT_$i ~vW[xTKNޖ{7gX \13 ld qWkGA&! F~f6{ ^>ﯮGO11nHaPSUqvG~1b޲һ)q+ 9t$c=)!9-w1pmL ɔ2>~AcM_(_$@.ElNByS0/]۸]챬/z ]IhӹΘ4v"J~#MnQ[ۨJIctkx?UmB648=>X>(:B6KF09?确G _loP]. *a->SI T*N!T4[PvJGgzp$5 Ϣs9H2cK4i倐ͱjǂ.F\6W`_!J:inޑN&c8ϞR'SRc'H{)o z*2rf|70#%fHs76pR00 鏆ڲ '-3 HS~ҝ(K+|B_kiyӮ^DW@b29>jmzՉ`A5?[Ud(4䗄-+{w7 Rǝo"e4;E FC>T/>ɰHoa/nhuI8UBæJ fnVKf(z͖ @8-/8HӽAcx @j ~HMQ : v GU݉Qjգɝd,eVڃ k/ssm}Us_ʆ#b9S EFC@:5zГ} e:8$Rkowid[W?%@GRʅ37p Y٧ >:oN[^='dH@GR|dϻaIY&*bGʫOIJi1W\H VTSљ.q-z*RD~_ Ƿl0J׀[l#@.xȴ i6lZHaCZeh&W}GM U>嵣H2$ma;E- hl=-|+U\׉SgPN݉ɱPpڙOB C^UxyrYpy%&-4MFۈ7!`^^-$|euI0M&t5h ~J7zP.wĄouD&D$mhwϙ0l!EЁvsvNRJjv jLk($+lFiϜeх{ȇIW0=3Qߡkǒ Ht^@6l&H+@b v/o$cq3r}>vEy`HVWe@g-ߣߙ9#</(,"=cyG J{(E00?%~lRhݸr:a_e:p?#sݤHWQX벮=S.q--?tP3($%Y0F}HLO yW&#! ؛;NGrHv'N²<%[h k2 pFᷴ>.lfў юa2ZxwIZ2Pa%Jqbͳb,<:7x mbX¤yS[ȩ vP8?aP6f*!歓%Ʊe#vyvG\BtWԡF|?Is]Aޙڋ֎0;yR`OWZP4<`TullV޻tS!E k L7XT+(RX}~b5UυSm RmeYCϲbyi?ͭNk*@|הF0x5lW^ZQۇny`i־IM}"{:Bpu879z7a].|vDYF*``<8?sTSήj͋zz2FFmiMq`+oSŷ>5F pՑz KhMp.`6g'LڏYyT*^Ô!@bu'^yhGq3$@$8q98rro7##Z oi& (1c e?J5#js9c L7D`Bdjp͑SEq˜|7CЀ`iq gxMLy1>0+X9/Lb{Pb(x@(=;ʩzwF*Z@F]>ָoJ`\ꡌz^L̲+Ba#gcSsQj::c ;oT p=)@d>N|QvELgs-T(9RXhuU*N,_/I@o32ߞ'$-D!Te ݑ[?>xEQ%8GL?wC(eJ<ńZIK  U4/P~X]ҏWv}yA29Ǐ̞yCc,d[5"--l͞?ʻÖ9~gyPB2TDIGF XؘNx3B[&U z9j,P(΄(*+axsXk77Wr*0,v<]E(͆\ $ %A7>rbTR\T.\k>E#uX+FP7m ;D\zw4E Ӯ&F-@jUIneE{(t/1>u)+v{WCv`}K*!?""]t&߭x{bzeZ6BD1Ov}T94#S97z|+?mgT\nU,YD S?F/2,7> 8!9*ckϣ䯦6Dldc 0:+\L{^$A [&Ƞ&K&wJk~ntz)ZN'O$q/ghzHI&-N-'ԴvBn\^0":8OkRwJ砖cDăL KDL6K&2SYCB!ÀI!p 1DRO"VJj4S6@\:d^*HmjNW1!lZX6!@K/IUT ` u묘\KDDZ) cx,WW/bIo)YD72΋{GBk< 2LPqf䑫@>vn,bLME:7>޳A *o͏dCEE_ \ ytsm@|Pko !C@'g>k pd+Ёvۣl)*[G(OQ-#T85 /!:X>aTcH`ݩ,v a BHNxҤWd^}F?X ǻx2‡.VF߶6.QPTD?6oP} I[PHX+ڔ t<-4?eO]Ԕ֒$/'kjYя7nr0` ^- Z;͚l=K uu5ZʌaqY~uM:˕[V;)r ]t,:m]Nx0b-|0:T.5a 7ZObtvb qQm,Q2&"Vgds4NKi@pj|P}j'yZ&"g`QMcϧpT(F&W*c׆1cUӐZ b)LLxVkET7BgA^y] :!"ta%5 z1(r˥f9lҭoa_S8տVLB..uiH3须sߘ,P^DOWD;&M h'9})ܫ$!lrR#IeQ)ϗ=-1Nɞ!5ӱ/8:bގ "|5 }vfJqv"Xh2b6>􋚣e#č{'E*~|:K3t׆N,$ibbbjOfD8 .Lf0р%o7&l0$trBCa;п)S b3m.5M쉭Nݖ ٲezb#[MSk "iIC 8i`jSaKdqs<:Ajcm P# \}IX}%yF'{/D dkM iT!6bW74-%w]y{/C'""6v|r,f.띵#.2Btooޛ"}ûW*u)1WRAcmܲգ( ":8gFWJ֓Dhe Eu)0l 5^yWvD]W9 .Ał徵zM+};qg iC&qο=&N '}TȫYyy&q'<iQu]eP旤t}#@^o szL!YL ~c=UVPX̜իj^N+\|F Pm<+ R7/"jvcO3t D(~4ǁR-Il KHKi)rihGCX9xÉX8A+;vqˡm@W¥')^*Z[hPPKK(Ikhoz䤩q!+~9{x(լ*^ ѓ_j NAB^e`DEo h<-ÑbnלK)ma»q6þ5nǏ%yf93&(TH~z-6ax[7H[aVLT`󓁭S̝î~C>#6|y,hnf051Zʑx w k;FZutDMϕU^|u(ρeZ-dT ׯmqړ̷/!b }I7HPl<^fYfEs=e7%I|߰M @1zM$Ixʂ*)x}_7;e8*PZH?ϵbse )80oZO\e@2p`KqlQ~bRʔpp$8: (XZ.± DݞX6-` ZԵ( x~jGG>٣Uf{Tb >8HM?@5 $j’[ f nЫe-k't$}iJT{`]X\,8`Eh"VG^C(s#gsn\}"-u FuwQ #N.AB[^&qyF![ BsaY!pߋNXV_c%ӖnQ jl:ҟkur֋~*EWyP2_ kAIb=_ҬNNEx&#! 2J* ;669c*7ys00ȪU#+jzfƎObĂ >ӨRt\ d'.kV_3|ClrB`T Ĵm9{ r^9r!/gZ  `g$TREj 3e>Ж BfZ]+v&g>TOFyB!a*=U\hp _RFSBT0)V)QTJHQa.^k- j&,(*$:9e\Ryͷ{2Tk/s_ qNs'-@KR7['LE2Cƹb[.^:Os]5 E]L/w>f :@f#3کnxQ[lL`#P}Wɤ&~S `x% xJE*w|@K#(Exv*{76ܶ)>{YmcRH3rp?&Ӕŝ|}-2[t3w YSOO#'|f/o0bsʕ:i[ow~0_djto3>u"|%|(5>$^_xo?}NAosC/GmFۢ 2L;|ol0*[f`ϒӟ:KeNnǷEf@3m*y%n0Q`&]cHkuF'n| jrLD94$R (.Wk59Rq|snݢE!5FP0}y&-K\OG SjߍBe$wbSSJ|aUU[W@%+?T^W"WfC pl\s%ܭVbV/6&"x'b:2x.NN>TDbk&|'/Z:,6 ]Y~ SUփ3OTðE(HZsrL+{Zo}c;g@CjkGIœ}vY(UC%k}״3HWnM2'63Ca;yHvwQF-Z97v𖴱9.KՒ5%wgW`V_xyB@ڏG~ (5~dn ]E}L8᪬a^Y SQ?:ʦ+Vyd^Ii8 za#e sA舼iq 7UE53Q}ʗb.q_ *;ٯ | !s rDzϜ6cL/U2wU۵Bh)q4 A&{}Yԙ ibE@TK4 :cZ\WjI=C O[HmkFWXWӨQ&eȱ/bYQq VşØR\ᖞOy+-s2P{S6@If.QׯR|A4HJf]3Tǭsmƀ@ %}}YE$A/elN=&| _ȀZcv]RWlݙk啚ӿ nbf<,byק&5ӟkglZ*KN?ܭSF9,]udIc>߇Q>HnƟ_Xhӊ5 } #agOؖdKkY6LIu1edf4yzi߷T8O"jEѱ%U羽y/VUYegn K;sE~AANZ~y[Kv_9y`Ԫ$m$w es΅SH(i\;(Cݹy֍k!+$PߛSe5A_QD#u`[ '1Eb[L]=/~3 gR%n9pܝR1XȹEGyFg߽됓<o#~wR[PN*5[oUi]jk"v>nW,V㡀 X~܇x}K-lNb缓BP6UK.:kqM80^@դi/JR9^ \Tu a5c$4~7)ʏ?kB$*:wNu.u#:+[Zi7%׊FmKa؟b,}]y_859"!8.u70th*(Wvq5Y^F,<+&lOp\ X~33|)k1( 4.wbVI3{6Od8B,.Ky[&ذ}glhQL&ܜjz6}Ǹ9Dh Pa2]``G([g&!< gPmhX!Rv٫ɯOC!ΘYq&fxۀS%2*ʃ tB)ޔحs#Ox;N\#Yg\P-S-SkoxBcpck9A> F‡n ^زgCN`Ҭ&òޯgh<}8Z_p`%*;C`tLWИJ"7GV&W8LC4t`vkDOwMNuV@ڞ8i ~N\Dr^kPjNgA'aEƢZ(Y XSJ5Ն+ f8֜ANoRJyt+DZij3ӴvW0^bgt_+j8*40yZ,V$ D%^H+~?3!6Cc┡+,F7?6{mXY/Ű|@6ffz 5k9DLz'IK#`Z&v~*})"End .o=ZBzy-N!Zg\3AD|U$ya?R?YW/*5P(4w vAIF^H9B)>SSمx+ڠ# X{*W+(팘;͵һ'TujtXRٷauOp;3y䘪H |L`&LyN;CnqHb2 '\5%󻓮b^1dw OT=ѣs&&s심Uɋpaՙ|"F$c!oRpe u@ s1TѰgwQu.QoMmP H|􁑭hٿfQB,5 dRr2v_#@S\] A#R*Vw3Dރ\c m8aԦuOؽ<¹owt|T?PKeTQ&+)#t-{]AhdA}wcz4`%xOGuH^aO.}%/3)f{j,&dp+d /f!7{,/ÅO*U:/营_[5Ww_F Z5Cqڦ+Fͤ5zz\'2E+!cJev8`QHS59W8BaFx1ܮ& ƞ] :T]; O/Qs{Q8H,%PEQ LdٲBdEV{x\0EM 7dɃeWgcf}{~yPvCAc1C뎴O "͋~Yrhz yz`^)|lk.#K Q1=sx@ \@ez2+늻 +qAK?D05k󚀿k4m]k'ℙS̞::ыivܶA7dzi> {:ElHq#RbSLDHX4)f3ja[! `Xl5B$Hx`4q$ V1Ր5k w݂E6Q#wsOXiL3Gٲ*y!\jt ;-^){w8&bN|quq|ߘv<{?% /P*"+@,f.r)y.$Udwʘ3[7_srC">vp k"Q:A_= Xt5Ë3xҌ8NySj-e҇>`b@ 7`١.Ak~Kea4ı]4D@D! hv4r ԧcJ<{|Kw~yt`x QNv:+߾]" fb|%R:Ƿ"z%d"h&c#m1.3^6QXV8Dd:_K)5}uW?Yޒq{ӤYI;w+yHF. ygvJ42z?1UzK]Pa=/YAɀ]V_e*of=m<m{>mL=6רE:tDQUw! |򾸷.ݻ6Qcd;IHfCbeE{S5Ʃ{s. EܰRя+5MmPWBKc{2w02g4|"6bq)Fyf*1"#M3&?#C ԉxy3B(_q}^Q Tl%<|+MݚEju؏YP=B I^Hphor)mD#o⟚6/vPZf&H:ob1̣+{mhgadϐ̤_Ygrwf0.Ħh/ّlX;Iݛk;NkORa8ʐS' 7ϥ %^@8Afoيn,6 B s-R/ﱿp!V36Pe̜Y']ilS^ﺛ?[hC"(xVvbˠy+/Sn&8!?²Y6i ߽SB#F-J>6~a od?^ ' niHr&Gׯ2Q˓a,`%Z~ Y2 /z:Ŋ eN5]:ׯR(}y5 h߬ps1TmK ,Gf4.*9(, FjK 1x ~T|]p6=3YRaë|ۧo+$A3fK'nknF_VVp gf|Q'l,@VÒu~\3$G (DYMF1v*gjx ٸmv}BBF˕qϿ4p|)SuxJiҚrxcS8b(fvdqIig{k, r.[)h&,ZYQ:=͖,#GV#:b51A &]@ NX9]0 c&R&uE"*8j3_ἯT0E~{[oa&leAL?vG"4R1BPKuYM$m9!$[zcxT[^|{LjlDPf/x|꤫L}K_(zE E 01 o̾%+&i~Bﷻզr: WbVI>@L伪 TlT1ۓe}]~7 } M)`xC>/G蒍6X/ީ N.ّԉMZaGSzДzM,*yuTD"γZ+j"U3kMĊӏ'wߡA6l8: _mw]9}DZ9oA˟5I|4,Hp]4Ά~(C |6_}Zj7Ff`jA} ]Ʃ>ϔD}HE)q/USN7xO=DXڜ625&'uݰmqI'7w ʸ MWy:Ka]nXAK7Yx=XWy}G&#{>&-< {P )U7߾>Y"um#Q{B3Am1?>'ŏe5٭?ˣrsQ30D?YTׁm̾'\VF_9|@s\YE^ "Y2~>**"QO\a"FH:V @ߜ\ \E|\g)8nN`R_kap:PiWM> HEz:Χ:[n+ EҋݶTTã],`),3pn d#$~A]$mל9't<@[LJ=,h fBLo h%ہ0BAɞǂjx9F_E~h+c^ HNBRL \MhQN_5mkr>* sl9,g.޳f{OV 4ozb'0˵w1mYHSx&/"ZHNҬn/Ӫ:3MvkWi T% g;p_fU$o&p"< "vCsPHZP5$ǙE+Pfӷ*=j>O Qp PKoQ'7o^`$?t~U 6e^B`rI\9 n3Rtމڣ!o;|HF6o^]~%n{_! lzHN]9&tS4ts'QsIG8!ꛘ4ۖFb5b{q-[8`f~Hyj[0S'%9 "Ճ ;F"2gE#D[?Iy&XA hSbh:j%q 7wH-[z2<wDOMN:өa U;*#*w\'jdyuܯ-u]a:wP .xe㣯:!ׅo] Ў3&)v00-g ~8qe}R(qJ[ܐW#ˈ AƦ>' npL}#C,XjRƼ'D >Zm<DR(wH7GUO+Bb֛,p8nu~PdY꓁ܿ1{=1Y\A;Xne߉ew B ǹ^~2\̍ʞ6E7;3K ѰK7sEGpqS.j(4_/COο&Nx`K.0Po[/'Ց;CJ@Β؛Y$+@Z bI嚥,8Q뷏fSf7B%.g*Ò*d"Ϧp1h YebiqH>F X!woc:ηCm~õ-pm@d$&bqŴazlɲ=IՊO!CxCYɐvsϻ8cҝ{=`E'yos=?*ʮOu Dfc5*?])9iV |rxu9):j50[l6^H f@t3/+:QrR%!9t! ds6Y0JB;K.%.n 0J~qMjܴ &@cxl?8IG*Gx $ k4WxD }a `BN>Ҵ^ &j|YF-* |8IzyKP_N>UMj?fVÂ׵; ' Bn}Ivny?& H>C~⠕K:Z=?qakIgG"j.vbtї'Ū/Cjâ(K ѕT7g xX1hƴ{Zli3S$O. ?YJۜF|y % 3޹㔞Sp)I7d|mݷ|İ(zZvfG#YetUv$If&`83C_FzxuG7w$¢ ?Əb_~gomI#|8{+Sԍ/Q6H cWO~s^8"-ll6IWl~΂)4,ʝ/2Vyf["LqyZ8[lqUZSXB̺գC|"P\}&AZ3`zE@38Y͹"1ȹז]c%A돵pniƣ>cbDڅ<aDӠ[>[ψ^}&r[`ǯ&u cE1:Hvsd9W?/0Τ xM/:|"EJQ&Ms3e?Q#1'zJqZ.ʪ_$=D n0>Vhz:w .wȚԅ\a7$9m,M5XFX[`y= 9 *Ҩ{Q #vGhzgfu8mOݱs/rٕ~bV :!N|ztާ!<>YKD Ϸ3đp: i̘0Ԣ$79;~3{*KߵaFW iv o~˭GrA|Jr&rw vs>q]ݘW!eaPZy$Fh5U+760.YtE4O37߶?v8E_#JЈԑ,j@רia]QW*/$MMoP,RL+uÜc]`-w .QA({L3'ur]bv;CE`%=^ zsk ~:}qa~K8eh4a:zJj(c-TRiI8=NJ!_!Q&(;rpT)J :%a*RG&`k*VO5_b-LapPfJaaBhy c3WtuԂH~/b*w"dRTnK'YGq -MѤ+2+E3CnvVsx#B}$@1m w-eQ}8ۇ0*G#Bnp[Ke 3Lp'Ya͈Igb*߿ x\O{0iil6Q~!Q,.Iyf Nq9NaW 5hZI˔LBߌ0sWC aH.UT`%kS F>1cIJY/xo3W .1K ׸j&bs9*qfj\@ԿK9S,zRИ9UH?=څHxD( lf6{媽e"+l3dzǼ#a' h(L#Qp6s@9ݝ nG~uD1ƥѷ8*H !2Wx & o\QX": [ O[rG-ZZ^2%l BJ?CՊ<ٺ G0X>kd/w9W"oE'C!eN%?GQ" JU$8yfy27p X6*҄Ɛ٬يPJG|D\lez?Q6b ˒[ kC1;}vK68/T(Έ)55?*gvϠ$45acdCsUyfWUIٻmS\5/1RWw]rʆM@׽`KPH9:lxȪV\3N3(rփvB&c>ͪDk  fnlG;K1E/1}'ːdX/*Â*~'ʬ *PʄQe[SmaUKhX[3'JP?~KwUj2fKFJOps]RԨS[ 1]\I?aƩC^Tҭ de(ECSJC$x)y 1+D]]`aq\+1iKEB~r0X'z1!~eShdQQbo`/zbRAXl ܒ@*0"g 4C+4PʵA ,2gS= h2I(8ƆUeF["ˀ?.z0ńڮyfdμ 5WѪ czwĩÛz3#h@% KDӞSA(sR&sX^^6NDgB$dퟎs-<ܫ?z"ىV;/$6rE;|hR]]s^>Q@+{%,1НJo\}J{ )s!^9"r$qWF\ g~G?K,%}Р"뛺'a.cJkw?"[ DW9Ș `0P;y!Yj)N}-j}CB~$u w@< |-VdGRfǗ煑[TPƊ9v+2&&5F/jbG!چZV bycEx,  U?ሣo%ct9n>f2ۦ.!hL^Azz I4\GIE`MHi(&ww~ ae*P;D`T^ٝ\܂hl10wTiQ[#җ+pDE? vt [~ 3PV S#n{]>c3x)C<Jh)]<[w+y_Ir$ɵ8=չۖV 6MU٨y3ɍ#!%uzfO$yyr #ri͠`SwҡEjzJD” usq2lKPve2ݘa [d\ƆsSP Y~%!/$e!C$^^X}1h$)_dHQxjrj|VJG9< 6^s(Gk-r`aUeJV/mkZ˶X7҂iCs6 \K?f[iU݇rnyKwD{ 襜݈wFyGdaØw6sA8VB km% s!k@Ivc%B'~ʽReS$U gm(|TT~i+nj{Wl,2fu".TԘcPs 6a@A\+F4J!^R䅸#1iʨOrLZLRԨ8ٶ)HbxaP(|AmRP2wοOSZṕA[]x,嫆a on2 ; MCG:l P 8A,UWLXb\M6A4b9ᔅ * ϟ_)i_oGwUL>ϋ1d&ߚy !th_4d557~~i9)e#;g^ I83҇m%8}/Ņ1_sP៊))uO{T4nHyyȊAERR"﯑D\ظȣ.#r/ײ(tA;h \I҂hd#iM-+P;)LCdݨׂ'ƂteAH0շ`{Th: YX<ױ!G@0h6uF*+3u%b3ģ$aJ$MZ1$&uݾ<}j[r@DϽx{_c 6XQy.&p*%~dzDadkBLX|O>{xx]4CfzL3syąf5J1ÍYfnɣQcYOB3he+ E;U%;YDz-:|:^sKC8X=Sy0/)%{n|\wGmRi\rxV Ni@H]ȝ4)=e'cMϽ9?wN? Z9H~8kDK1ZEJp6 M}9U8ޔbVI;}{GUCYQb:%q| /y>|9:=h1nZ~8b X^)`4!/a}b&fh@n}'ATRPr{+_X:5 vqeΕmTt&I` cR8\[/"F&A_وDCo:T4&$'c}TkG%Ǫ \+ڈF80IJ&Lᮉ[XvNy ؎?f¾? AwebpaKU"JG?^3h<6DfHr8VP$;>jgSgRmkHaj&w0pr8cpGA<,ך]/(j2үMK::%RZ7bFQOe]>Yΐ0ۼK՝+A$KM9|lvK8D)Q']j1Qpp .9ò[]7%ٌ O$kAEs"̫t.KFnV[J'1L*Z/'(W`#le8.SMȈ"W!qXhGʗ75ζdA\˄ 54sLOwxDk?PMB(AZݑ^خ簼]s ɉG=S@OBPyPb;'K0w/F5nBfEǏٿ4!T9dcPx c\`n Dۗf#Ύ##SDr*oZ9(]6gU?M?A:!ENƔT ^z+%P=͆`lǣ0x֑$qߑ"uoF]#asWqQTmh0˲]M?! Mr|RV @dOuoea4Hǩu-!bѭ"& 䰵)n`k>yfTPtSv٥Afm LR[?Gt κ'Q6"0FEDJ}8!}*77xSUP [\2|Koa" V29!Y2i%F]bLNՅ 5[?E?Z2+Ibv`1_ Tp#XNA׮ !$a+$χ/oB;̞QMbKjշWGnx]ΑU-5қDjWfaa" {L& \@a3lAKt#]?*XRov&Bx|5tYL]s0og@YQ,u*~vmv4S=eCK]Q<(eP Ȧq*)2V"`I;5A!>] moQEeYNL<**Q<t֓@$照.K9[M}Wᖚdquh(9HW41ӖMv*^)I9ΟF!I(}6Gty@#biAzk%bF4LMCTM"q,Ozm]ݑ0n9fOYQ-,^q&=b&;䦦ZLƧ{^φk!6#sW7Wq_ݗXxl]7.SE@%3,Jv/<1eQ%9!s,keqzvLO+F]&RDkm NodۦQzH5,]|S|f!Vj]8H#۳ˮp y[?,Gڟ#خ8!yk=Ј ׀\eo4+[\W{S lmBp 3#P[]b[m~?ݸ4R:=܇һ 4E խYʑϵө/rO2;|ɜb檧i(,,X ~.J15lLLU:J>F>'S ދ9,zMHW~K+QYa}rC.jA$[;I?tB*V'зE["@萋/Y=oY+J|[S%*& Rg@?=sXBqno8pnE !70pYDK ڑ'>p \캲cٯ HT̸[A$YP@-=lZ/.<~ieݮԩ| qRkd^lgPLœ J0gnCZ'7 ~/UxoAN@hIv&7r?A~ē®ρ}{:[]|TfA̞4X$\z U7Sś "A9k2qjqiV. {GT! :'H`~S$xWe>l>96;{;[YIۇ:R{i@ ,-lx[?iAzi"cX@l_Cj U>[\=Rof"YÜOc8wu8mZS4:AGQ&c T`:Cv, jL:>B%+U]ɺכ,8p &xipFj%&sv3Ԕd6,9VB&[/ K5GhRPFN>\ju*ݷT>t^ e9o_-?F_CȂ1^{sGmH_Ph~IeK27VsTa!#%HH,$cD5[[?8Lɜh,C2g G>78- a(#Cpbv'Y>p }+]eaT%$; Ҵo3g9&M$9h7o۟e-?iE]!B-FZ!wRK !ZN;AzhGL./;V ZkG,)lձ? 8yjyR`YvPʈ4HL +H~&^*t zx2䜳ڢLj珟R n{ԜVsQ]deR#zx7K΢< ;e.OK2DF. t/7$Yd!Ncq#,7-,9AwYrZ7:?}l)(߷"m wG Z?DqT7?PoȚ%9vv_:|؁n`ɱKk|w @}} j׊,0| ,l$r"es`ZfuLbQr~ :P?}R0u{K]Uk~xc`,xEohSa]a:` ge.tЍ;jwߥū/5jt. G5HY,[tSCU|؏Jg<GJtUxa?"Vq H!? .>٥Mm]d0Kf~xJ#3Jc>nNqR /-[zZ>X`e}ěġnNOW)bB.}/ݕD&vzmԺ#yO@^q"T8"&T [vH;Jq/D~f~L+ce|D9 7\V-q,fw{ Hq-nK ߒ'YluH5u؜Ry27ʥ=j#0^mo,|e'?3Pp֛9̭02 mHNlI]{rR)m0ꙓ\dssϻnAv`Mۿ񨭤 i2JgKH:2 uܗc  vl5Z2}k2ċql8xGO*FsI C?6J*t)v*4]i#CzTKҬ8e*/Uɹ18}I#؃Guܸ-Xݣ[]t Fb|7/rG@'Y˔WFr sXqE=uCMT]A"%aŭ㜵~@Tf ;Z)X7;Z1ج`h:p35"*|!mj{!b "ơ{)Ϫi`tߔ6.H4wxaux1Ǐ\off"-SpFd'wc?.׷}o }o_-/%ӇzhmR0/x,vTBAkʡ[C^0J!y,!iOTaGFrS+@0uzQUثْOuӱDqp;=?u'2V^wf-x-2vz7w*@tq ]~[T碃 0iΚɄhrh^}"@.W|w&ZY!QtHAX5|;F 87BwE ]wP}bݨeô{Uey h:V2>N=kYi. vxRe0P@=Xb8qV/Fr  }y&;YqV]]y7۾^)u8=5uvJ|0bJ^G {Y@pBnz=;]ys7o˔Cd b|XR&c`؜xxM㴇@7{%_Lp!e.DhlJ\A{D 3*"rC ؕ\_lMP^ hЂ,)Ң+KݩИ|c_ص./NX{(UܥDLcj&]? ?|{w4g#HSq~;JXEF%UW.Wqy)w!8׳ ?T*hޟunJcOJ:ޝG\ R1=`!Fn{;P+y4t )(ݐsw -S}۲QwwWnw*i~3DÁZL#@BDO;3EmևF] By{gfl\mͦ ;S_9L _L r@t)<`ЫpWg TUw;A큘M}#/OLS#uAQ<g˖3oMDžUQkF HSg7p]|GM",ܪ|b;yU䶣U'i>ÿح:P~ԦÞxf Uv Dzc~lI{^e9H5:z@R- ѳpd'*gc-2Eև ̲ofl٠l[:x> fw5/MiVtek/(`=+5-] N;K:yS+-s\bJT;Ugd'H#}wH;"Ǥ~jI Eddž@镗ݹ;5K%H0zk7ƇS>8XM嵎=B ̊gtCtOގx+Bc5N_|O֯o(v,Ab(,=dG Ȅ)jZ-yu|e;4:|Yג{&~q] Hcn65!c`oq+f-۳kvp֔ ybkWTsH0oʆI6>3Y+$)N(ARNqRՠ7&YoB4=K~@++o}s d"hzJAym3i߁|Š5n8IH K*b|y '>ZXhGʇa\ex aB,G'ũ̟nFLc9چXQ\#.H/|37/ȳOfpw$(#!bz໗sP/tα"I"c1jbQ0k](O TI (t6OZ{n,Aꛓ%̗3]&%fu^L[*jJƑ݀\QƙTz db^rB?\KBdxzM ScV?eԎaF0Yg^8ݔ98V9˥ɔi֕մw3U~I - {MFBn-V8f/ wf*jt26]6v1<̭~LϘV-Q''<`:kQj1]۾}H$2;40J׉4<7v"5;h^G^_UCbz?>MI@G1c-Ktw& aܰFF!u}|En'G7\ӵk["6$./ٔ\s*ŀzzx>W8BcΦCvGn*l^ W^ڝϠGydSжרь8.;\+N}(L$*\@U__.f3S:ë:or?*ϒ#\cO*cUwX0_--r҃ M:4Ӣb}gzP,CzAcGFk4)[?}p _^O9/d[l]DO;=zY.7ͣoT؜8 ICY0LG=Co]`J.-}Â75B= ?^_^H.m;$vj vI6&{" WfqaHmJbR(3E+R&~+SyZ]FMry͑޺RInƬPVבUvFK51Ҳ~pVjX:AP#A0GdSQږRRu Sa۱WY:k@:O:)ͮ?j2'ʣZIop@>r$#%пq\>OQcDB,@'J^k]yC6h9 AUP(>_m3FvɶOQ;؝-ΎFin"oo xGLRV,~ Fm3I>r. Z)oNuTBFW 4&?0/zA^QhȄHuzD q%xB`+}f_]]`lSDfȝeD{9Yf`N_h\ Fkq-}ZQf^F!a\OYEɴ` ޹N)mC.E l;szOD]vH13R1D^hq\kCTː%!;0lٞēP/9Vٲy z_ͩ$79׏0o1ܑ{.8 45Ku~g%iT)e`: nt XȖ~|&SȚ݌<;_7>^ƠhnьQʼ_֡D3Wh7K `siCrmlR4‹OeGH=%NlE5צv?3FEwdK]esJmKM nQ<8;#RxIhqkסjS z\V>NWGBPJ=znx1WHɾ^&*j1 }I-/kF1M ݐQ:McoBmz%&ă҂$=o:egTHg4z2J>bZ .$~p~lɰH@n5J^fLmu3ʽ{j•A_Lj=lq fA?}|ۢQ(Tfo&րeDT$[wVYmCqiŽJR?HjD̢aR:yH*3z8)nx5kI}"H.n.٫ Fg=2qS>XUT_xkP<*hO#yheOe]@z}B<gih'lW2n0<4~IS<RɴRȾA$]:SHOpMu"$ѫn @RQB%!#M{MpFl6ve:*uܲ JҠ}0(bè -ֻQ8CxDύr̰uf%m6QNH#'tހi䳽6J l[oIJLQAHUUC:nބ|IYB/|[\< XtwlepJiJ텵tSɏo/s=x6Oy_,|}՞9Mӛ~Y{w]>͟s$cg~8Y ҦmROw"|ٮmO-f:=J9j9qܞshLk("Oa^Z ^%ȸjq@n|#s9E5:؃G*VKeu0D߭_!Xbyxpf m# ā5*ʨ#% O[ E2ؿL7"]xuC_-F3B jAYZkg]ۣV[Y.USf(yyS5N *%MMø _n'4u[,xB!VJ*?k*MPG-`Ӟ}{T K'aiu;M %R{X'n0P:j͸ DtNZ7OGWʗ6;"N뱍 4Et|oNB-u2 qt/WUpoqoVm6.s| 5-US#KՒmɭ 0d)G'y=F?:.?7UW_֑b#Ƽ-g~\ h!ҭȪ~-vڇg;[%BWVY`#܊Q.Uy 3>qêN`(>ots)t[Ww bW[j Lƨ7ʢ$DmO5L297IƗ|o uWt_ũ8{?FȮOw 2IG8JuVM.{qߐQP:$\^ 3aa; R}n#ˀ c =q)=,4H#@O1_Շ əWB/ƀlMx}ԄGDu385$@Lkѡd8^9\XŠ4$\aY BG-üx]M|;D(LemiqݕcoY`5r&urEA=MoX2> &K9d1#R}X]S0tlmuK=ha? cM~ɚ @kUtz 67} jvAq>/>ӖIʁGinnJX u1F'96)[HQzxM8=IX?P$zpNp7,W Bnkt^OiT_v ȫyR2[Z}Nr&Imp;ȗT,639dԩ*kFA d3QY q/( EF&*Y} q su%AfA! Lb/}RYOѡv0  HE[iFc鲐s1^7̟ gh܅X/}[9avQbW9cmݮrq&WQI馃e tN*W䘜?M)E2BF:B'O;niZ[R@nu ,M"`X0[J)IJ?~m`Ko{),R7 t/z [%o f?edjymV~^jJ_p@}^rΙN`ߵ[ f#cNHD=jh%ނ[~BE#!O: !^t\)UDUG1fhi @o%PHt\׽(.%B;Ɨ9][#1 Yy]XH׎S9-s3tvF1 > K9'>HGÇjwt[2RZ9w)g2h&xK|!q8UŸY-4qg+% *N+z)y.y t”#TU8nۄ+LE~u0-맂y?_Z؃D؁ٗȒGXp]q ϸ$ēpjO.O@Q+kB5m z| IR]|d2I E [&vóy LC5"xk7nb C$hw8:+'6/s-%w]]2*PYK- "=)bj'z\abtXr(.TL'vL.SvI@¿R)ږEjF0,5)}s.}HpL^g?\`q4ՙzXbw*SJ1rvĕ^2!kXAl& A2>ā1|\Ցj'xM|i/ъs( kv/=]DHѣFVʥ:;…|CܣF&@J8Jpݨ/H.q?gY.>K.^ ʏP8y(Ɇ[PJ7x; c?DwSO-3D >d.d Q2p4lYڮ1oyh<D}l5o8](5~Vй3:Bq+ q{wߍt/#ww,2(/\lt`e2g_1r C7z{o|مy9hhT E{;PܒTKQg\/ *^!;PV];MbX.9UAܵ1þG)ڃ׋e(+n<:䝅@4 ? 7Bǟ&o`=XL38ِȊ"WaCIsZ HBk\'z kk{)&5 w$J1l®CoǦL Ov1铞cOPY|DJEI=Yt|g .# '4\jbJ&@} g |ߪ\.iF4KQtB7 |'ڪ4>4| <>~ ؖ=gnéMr"\I4bHC2l8%}FRJ:\_R|Js'®Ú>fj`<2m/Z~}'DLXNi`Hᦵ].蔏 쪑qCĸjF"UECd;u鞖Qf(3dNso/Y6?1jX(bY/'6n-̻)ln/Ҁ>ޤ"-ϐPo8\nksmUgkW^\x66;BuӒCO\`umtPBM{LzR$vWg+ /_?rr GhA=Z$FzιRbǬI[7D?۟@̧ N~:VKS`T:VH=oo'1M݂qCd8~w2J ρizN#b6=\o+d֐]!޹]$ C9SxkF qdQ }V9 s ~$\IރRVm04^@QY跡Iq^{n9ij:EѩRc|Gtrr[WOzx8Oh:kv[Öx}+f.q"^V(u6P-xYp5dZOI$ Nۜ,@a0{mБD͜jIUţ6m:2B)k[6q$f;Ҥ?c,vcvvJwt/ja5K2e釦̸kRTߔR 7]ί0nYpOrniMxΆ&Xg% F-02Zf7_+rC+$z-G֠כBD ESĻҧ7)sxuyCd(7S+ Qw,w;?ݿq$DzAu( |)~7`$|s6fy^dylb3; AhM>!OUt}V!Rއ+_(Y3a3', M=mŠ˰Rai>s|R͗v# #F^9%XXR#aG#Tp03I ?25sm, ЄƓ<"E݅bDHMM8a/;bCsmW>Wv#Bߨ 31Bޙ+^M|P\j=KteAhapv"̵vK,~ nz]F:̈́)dok8D:6!wLyS|p2ôR([b^oBeݡߘ*6Q?uzyO:m;Y= c8 S4W7LwƢnBDЛӥXH(/¦;5UX# o,t,bJ22HwDm DNC8rae%t`eĿCo;a0p} jg 6NZ_ج% 6غC_QFA#G{MKsm6/$|譓Z d%8y]S7aSGVѴ4!M>S0܇Ӈ]fAPU-"6׫Jzw072ؽ4=73{%e?dTf ,^ڟe7@4inB'wW1+/ʮU=@Lɇ+~Aխb{vDab>-]Ƹ;;SVٿAp'zOg8.5qI7xsN,bcӎl E1^^lb/7\u9q M{N@CJ$5uQw1ccJ}˕! 12=aq^_T0d^ޙWsٕV>N#P[er0>"%yq Cc2y5mn(I9 d\T)=!OlljQ&j_4kmʀ;V%5( İk(QdB#_wׇFa܈3l>/.(9n|?VÜMTKAKo > Zr)a6gu{ch2F?I0Frc/Zm4rﬞ!;hNñET[)0e|"#eyJy=~\նnWcD*Q Kgr\ >g|w6wzLU9ۨ7>z[0xCd-)l4ʽ$xT(c/>=ʷ䙥WC3Ia=[1]Z]]a)ZR>`ګ#//HNpAHU/{Gjl-(BW$bUpo63㱊wߒ$&K,P4t?:G01=9, 5'l d`޶W(>t ^Dv?tG&c2C d"$?P_#Y+elMMEW:vK.ey!I%dɣ] [.hX0'Vc:gKҭ l\`p 1 )()D}C² M!TK4}$zOLwIJkMy8Q'qY!1d6t~z5 4JhǤЏk@q186#Uj+hI/aF-툥_#zWu;rk-lU>@ _wKA(@-\Iygq $90 x#zg}Rȫ τtlp Xt*;;[K׷jwRIHue;bB!ŦyB 0̈́L2_'lՋ'Vdt|0ѻ\S4繰T WmDKg4;dArVW%k6)a?%bML<49?Q/`f߉S0DBއm#׸ @8( P K!M|]qX8~ o!TX nthR3YLt+:tQ*:NCoBbհ\2م&MN 7D8y;\"T?+zh:nX>ƦOTPzAʄ+Y52|2PC\|QTJDȥk䒣r4NLmL-8=/:`[Jpyw j܆dAxN(H;3SiNP6?F Ư47 \,Vr6[` M JX})ڂ1֟ZlS⛼er^C`Ó3)jDԞ,+ EU7 (˗4|pdzqEvdoZW$`}mXIYA="sBؖ>MP]1p4Q_-3nYH왕$魫#[l Gތ+b^ZVrcuxl%AI ym?e4 ac+(-I].ǫ4j =Ƣ5މD7qma)3~uU<c&qӓ]:MG*iȮi(12:uȲ ?I qoYiE1N0XX ){Kx7GKF:fu;p9}xEخKj;Ef;.;$zXOOӛq<|C1uP㸟B JĤ_-TQiC`79<:mk1nsv_-[q-AkOybL4ȴ1HvzC&% g'kݤ'fiz: { w3',N,HP`JAn׮Öp"Rh[ڂ@Έhn|.<90CC!tk]F5;O(`)#AZF=)9.[ֲp\6BhLRTj TwcpB1MScye{t+$˜圕tOI 2I_EwKH+~j傛tWcF;19 Cۑ=@BcŰ ?FW2-s>~3%:!D|`rPYuHh[@PO`Ҍ*n"#p쬉0pP#8,b|Q[VnE:mtnީʢt8~ Ρ%㨽r0ͳ=Bs+YVi@[ 1<"d*$夹~~ @#~X$mrky 1[z~,E)N>م^[5elU澊S涡MwګߍQuXluE7?~{^PT%1s)tUH-q cV*\J[_ܬXAzUm ič&5ӿGvFiV$y9;rn5zr˘CH%##'~W|`b~*)1ĕm_Tvc H ?VNú`8.b<{z5gJoe4ٴ _`8/lгP,rA*(dwrf4zIfokH_ "-e@<ېa\Q  (F;U8!eAV7䤰mg)}^ۗ>?ݩm/{7)wWS'dmޕe|:r9kx/\NOB A7uGih࿫ZzCX4F7vu'G|y̎ĔW,í</}vx%9N/j1Q*EMBKTKVm<\{mvyD }gզٚd$$sfvsiF՟v\9ro5AEn,ʅzS!Z2_gjIh2h2f0'*tX<\=o+G8(`&03qP{}!gڌ1r_?JJ-W)\gZbi'B]eA (W4" 12$G缺rC 8RLA:0<,͠R9pԐu9maex}GLY Xm΂-F}֊,i],9t׍pQ[/ |˟Pq~_Z]'9NOF@."/^tՀ]eTyyYRݕ!yEJ~m<}Pw3@g/v*t`Qק6<ɬ== :G fT0~\:%vlB4as晴`{vR3ʖ48&墧4t>`eRxopWL`si) Ou4~U~M^bkSW%ԓITXi;hL*x|"g~B&iAbήhqxu8 ~Ȏ䅏cN3 !-jİss3tR2 [Nv 8#OjF)mwB$BCc XgA^~h|KքnIF:FT9^֤jWlJ[2B2KM{U#Hf9k61;+˰[ؽRǥ"JJZ P ˙2P֌G'nÅgN<8u=HL?_3)1uTx螚 "и-/vD!l\)6d.bD// _'_w<\>ӏlѦgSl?bbRa?tDFt(/`VE.ΪB Y*fiVJ晛pgףh+@p{\~~%Ejq(X W7\8#H ZW"7~@LdWhY nYeKo?F?rbveJ'6['A]e*pc [!J( 0EЇz6*juT=*<`X%fY,O'; ǭ~L"xR͆pSbxU6DGs(EH=dQj^G4ƕQ fZ+ DqyU2uae /M.Uފ so-wHs#)rdc  Kjny R1O?ͭhp%77QqSrQk\ "Aȶ?,3w+K l\t O!]6f"AUiKuA\cɼ ~}DD׋nftsyU~s)Z11 873{qvP> NC#"xF.KzIwc?71\p$k.owITB"k*Bֽ?miCAmʎz7r1a9ufLdj bFɸawi=;lR!Ut4P]0U,d~1 u@iuwdI5d*_ ؓ]c\C;8eڒMڼ׻$uTLX64cgxF\PcĒ>KU}p=_ćn(P|B3b([mTk{WZ]r~Qe |s8RlN1 $}2Fzȅg*4"D^70FɖbƔDzO׍  f}8uZ;!YJLe,<, ` FV!LHޒ&s=<t򫖅P\TOH3M|=D{7}T'HnfM/9^lԗ(.A|OFh${*[$H o=pAw]Cw(`x TVv@kQnXǵ겉?ɻH%{.zDj6sćE^8B1^ Z㨆aGCHbHw:)U ݗOڷFNcU?FL5×o7 W8D5mW^ܹKqk sR XM=ÓkVܣ0a$l@X 8]bGwx$<3m.wpWRV1)@~-mȖH. P_ E6Lw9Cq. @UL;. _:>\ޔzi 5}dYZű nE# Td!d>PQ"hc/mLӑO0d.Ȉ/&+#(ߨ\c7 c6‡\)8y/(>`yS1G4Sɑ2n=1ӭO!~8a h){}c>yR$(1B4ogqKkf!f>7jXC e8cq o lQWV@3dy T/QNY+C hEk NAR7'tԼؔQj^ oz>.A|=6|~-嵀wV43Ȫ4#Kי@4bpjX^en8 W!se7>4P,XIX:6d I$>uq6$R9ʞDctwXs<ye<2<:4JQ=X1T{R5o-zh)8OWV6n Ȗvp}:]Gb!]z&Zhh@/ZcM*,%^=ڀZ ۥ)yHB, t?~3~f;r,QWa"<ͤ~>ǁRp^|5>ꅶ!U\vԄ,Q}.B$})85M &Kag*;\9ׯ܋I&Q%7G?CNÎ*T\t]g8~7D{rCX:ˆ_.(1j"fAfWv24P"p<|zcDG8>8CY׿ǁyJ5UCK' -VvGsG:3X<_oSkd 2=` R*.W2i E)B軆\\G ?Ώ PU,#Ե3\d\^D)rmגhFUbNѹ6=g%pPFcn՘S3)ݝ 2NvgO1o8N$-Y<9;V7j+_B awCl$Mኬ_JxraqsAq:@9-N.QM S9 _+ agҵx=\1n\knΫ\mNKzhe0#EQ:ujicDT:{U.bzV6 dkRAQLPӊ5пt~W-\Q ̼N߬۔ .T'N_ŝ&WJoZ56BV9$!]z.CGz E\?ɛYPSE3 0{icaQ< P7q EO)ܬ0.Z4KjY͵p'UO-YKv5u@vVʓ;/VF>XcQT'o+`}2ρ*Q'B&j9Y36΄=Fu R|utN"2jDiؼCQ&/O w.|@F*9$\(gN$mt`}X?`k1J}fVPaOR`I ;k>ɵKs  !( WcE ?q0[L'Fe'n#w< +mz5VB0E dF-Ʊ,?+`klhӖ 50Ε8C@ӈ C^FO{^k0z`cƇF0-W/9-1)t/"qq@`Z s,o6#mŲMNAJ,6SԌUӍkNȢz :@B¼|!%JvODOl*~Zb'/wV]|11vkc*; *D&ʬm2{ͭ\'24hݯPYOϡ#31j`*{`7huJ ubY4y: Nty,Mq"2<ܤ?~?/q0kHka@:~c Tg>QH*K&/OVigVxP-F)Z̺8 % d궲%"]5+0H1q" ;NЬ+!/ɞhMJ[ fo]lrIY2r=K-X PCڮG_aqxZ/K !+ 73iV[wҬwn4\Y\Qt=Ӱ3*v+g(bǪ~Gonf{&*Wxƿwr[QFj+{F<}w)h n:XwPV"Ez-_g&gfúԖ;#*rVz7Avޯ Sx.t*\o3+n*G LO'.hڮ5AjVcJ]GKG=:,ݰi_=`HV]HqIAX T &d:?9TY4lvr9Yt;cpuS:I=cѳCh@[Be> RVRy1rXgfC>(wlNlѶzmI-CCd=۾Q4- u g>Knisd xJzfFʹܴl&UÉ]ފ~w*š^jlɬ9v1F [E-O\bSP0 qx+37Sv"E2祫f")]>i=vf^s9,{Bnў_ʗ `>4GLoI;.[\ꈲ.Y4\'꛰cRb(̑wK 00- hjSq cv'So+:W.6.z/ShE)`<̒Q J3cNh-g/lFM)F+$ʣj}"FUę!x4h#sM]`SlI,Oc|&lT4"êw)ֶRZ&Y^vwʬ>5!Epfԣz[G6:u?8UUdaWJ0'1{6i/ݡaEQ52Jܜ._C} 8Z(&w-3m PɃ7UR/Š壼˩b60dh& ˟2ytNuݐm%ҒEo-h)pbꅠO˰E҇#g<]ݘ1S2hL)L4?> ZA:ɍ|+!71^}^_ `ZVrafNG tn;~tAk9%|7C&OT#{B46-M qA<,FNrcƺǮy*H4 `@!m7cԼ1ei {11 5߃]-^#a/Z̈́.Sg/Ń牳w|4E/aH5~+ɾ~[6kiEVrU`p.<.#p]` $_D>V fv|tyE>C"s& ɻkxKm7Ά!㉝&"Pޥ.*.),"c 6༤R-ƀ4s夎*)IHwQ 3&Sd@&ՕqayL/MtΕZ a8Fy"9r7U*K !9ƝwW IZNqNvYmijX^y 'a.ZπƳOkuA6V܏Bvg>eWg*~f?^v/2f1ŽwA*dj1Jw +9N^VšO8t s-#.9N{œQ{/Z S>i;F⧒*F0_EhwaȦE/i+z4'q[zi:;umRuvZ>r)d8MM4פsw:m$*U^d%j?Tux'H}=K?VlZ_+33'Es$|5b})`}-@L g5ehl_[&.D?cÄK5׽uQLWDGM"5QRi;d2! `HWSzhv$xc0pwNTZ1}X^IfsW2wQl2#M"Ƶ16GBVMU3v36\CIrP:9^^$ DFυO/vZĖl%;yUȇٲ ҧ$Bd8 |7;p1&wIA""TVp?^:6QmrIDO$6|iKC}@5qH~ڷC+(e qBe"˭ekFg4([)kIAn"L= %}Y407~ӗ-N vǨ01d 6gJ~i/֧ڗOoj!7v g5kJ4~fFUQvW\kh3F ܶM}CPTX'G\cSٟxuLUO[5n;0u#(srw=r:\oX>+ WBw# .(TAÿc|dK ?&QZR^i~ Tmx$;MA5R%d}Fe v5#ЋZZ2[ǿ'^WCvk6Mz\'"yҰ7j4sFuS2@ZQߗQ]~R>=JOE >o~'^ɟ$;5bp W1M+r lЏ?]+dwȱ<2;CC%G(E3X~U~ؽTPNOXg 6]w.X(Pol̞dĮɘ!'//f@_v` oE@!6P=*t`. p~x1C;ɧKPs%t6uaW[r)`4l;g5x[@"ĩ\ʶs -~*|H 7T?Bi+5@Wg:ыSE(6S\)ؾ %JŚhUSt}'u]p7"X=(Û) }<욅ȇ;$,2#޿]i*ː,KY,#>. czy hxh>Tp-n**CZ]U:םjUENn W,$}nSϝuu!]t B|c}ЬJ{%관H_Mhd|)/Af3V| WY.4{;^Β`UcMC'9OrVŽ ߃B9P\nIъ0]^QXQ˓ ,`bdVߋ_P _yyϫJ߷l%`S$Q/\lihu-: l]ɮIߥ#K6Y;)XȘ1aCv<8F J݉ϣ͟!y fB[F=2%,l{=NǛu9eݕPf#e|G  ٧:Rf:02(}ʩ+&YA}r8RgKl|Aґ';A߯ѢX6e]8VN `urNE?bG1B+؞vT؞n^z@>=$Uh |wۭB!$D'7Ȗzq)-[TILvA^7JNǟ(M'|ud ӺJ6om@iR"#OPm҄thWwx`Ȃqgitz WnIU4Cc[βki٦ r`oyZb,[;:n(Ƽ"V7$X UfҘZ{:SdBfCwU>;XٙdÞwûs2#2],4v25) ޠjXЌCzm>)ǰ (H;-- aЁEUK{KcmG.lj)T(Dm!=! :~'ddw:_ETjL3-jWLr$ CsZM!~ސ$xƲn,.IZ߸7‰dx2xa&侶qN!+2eu/eOq$N!\j},,[%v8#P{&K:{9{+qΣZyrj*v5i%'by׈G4klH`L׸~\AiXIkj_w;b{0`xg(<3 6Eg}CR* u/n2Q3lH7޸:TYƁnxDyJ=} eF+¦/XB'*f 0Cox8niavJ>̊ь>v w! 3vxeN|'l>oၶFK0z(q3 Kߩ<}Bc!mƒþb@F~o! a_L3HAc2v4 ѶP.bȅPJi!\vpK$^^ v~Kt'(pð@0`!9: VXG@%^DA 0N٪΢\?uٹRY=P65;Tһ0 ^,! Z P>uPoH^7!RXpz.R$m'/!hӂ3;!葕k*+UxaD<ӛ0V9sKs,)NL-JſūΨ p \ ]vjHkv_xI0 QU\ q&1WwTC}[;UHH+ɂ(q.%j|j_sagv1)m(pOH{8v|f HBK] nB!+uCF\Q+il THYA."&`gRj].C(NMv.d(8~ %|EʁBNv,mnj8U7ɢ Q3~ BAbCq8?z[i{ԮEcs%tY&&o k<8{)}*im&eDP D8(.09qxY,W5)d)G.ߛop}s_,4_!^P&ڪu[rH yTvՁ3b`i,ǟ*w4]Vʟ#c7N'Si^R ibohZ w$ VWlnV? 4"aHq*˾2QMڲ'd4^-NPTs o-Hdmf=7.MD@LS"IZ^-h\t*s_ÚE6w {p@D_EηjWe&9.*8Z= ) DQܘ,(n9]-5Q|F,Q`tG8L:6&m\^# or*,i I8n8zDo<5OH|E*0d2]UOjZʲ+ saɲw;I8^!@j5t>@c?i.bI4'#kw햭q>т1Syo Qb'e){͝*P,Oizcz2i ]>8-|7!qVؗTüzb0w?Eڵ?E3ށ b7*2XV.fMA_Ph"H,G[VYOZ/@`my@Plz6?9'!Rnd"8Ϥaۑ%r3Mϥ`i1mNنP,$)1tnb'#I7D=Rh |IIdҺc"ِyꨟzX4HS,Jų/ff@|J?mvpww/$g5ζʧœ0t_nY6"DK TAb}yC-n΍71A/fO_"F xT:і7߶։ƙ zf Kt4i!C(:I$'@y-هѵH^suq-Uz qիWqC܂'rqBX^<1.4X*G,?$g|qtR\f=>[J0;`pԄXo hDr0—*l* v}-~F7?T*o=ozfy*y7){Ow`cF}Nh,LVlk݂߆AK>妟:gOh4mǝG{0o<4݋f"/Ԯa"Z VIŨ0a;L ʻ _wdJ蛍_AÝhdz4@v BFk& *LwԗF袑 c>Ĵwq02N1'*Rd]Td}wZDMnD~Ɍ0rrwvA_ܮ/&<ل.ن {6ÓUFbAt\K=,ܞ@w?Wß͞!VX~(1'FUǍ{v+1E-CU- ǫE.É8=~yg٧uBtA)Aj8 R)"WCUԭ /1t쿕{h2,ʹu¤*kӴ#e%$b^ި8Ccn{V;?]ơC VqWХ EX$ح`Ġ1zx-en3~0:Q({9D`RaL5p5:a2-zx@C,jgGoRs" ‚D,8Ok| F\X[׻5QW9R75昃 HO~YfqB&RRBZZ;f^a$ry+U}0`xlp[=ᱧ(f?l-sR_bgoˆS}$ i[";KI|Og&e}vP5~2cL޳-$K/Ձu&QkB*).Όh ʶռh9)0*$șXsi-5 V+t-1Du[5 =yS8R$ H(JҢ࿛~{e曜6֋ɕ0܆'QdE *7tRy pS{5MSG}ؗ1@* C|9~v6~- /# (~<ȦXP׮MHQ%*r8lbHF3lNQ$MS/sDh >hroX@i&Q+C/o eԺHAsהRO}!^\ ,C>P2"T^,Ĉ95؋86PFņ9pMbYiuGUL.XDc_-8Ct1PqAG=QF W Lp+2Mnf&+KrsN9#$;ΐ򃍟S/>=ƴ(bVN\E[XZ*rgԐ70H:ӆVh&--IYDž9NU}X"*G$eYRj6/_*{^ͬ9H}>f OJ3NïF"yC"%D,McE6pB;ǼǛ~LQ OqQ1K%xB*z+%łHP'O >$Ag(:.~c.03 ǫ%5J' =WN;tL)G^JȘC`8%vT_OAj'\V;̌f5\׫̜fj/!{=4E\k'tq0nOrVf$P (x{;uY֬b;,i?V;nBQﮪ+o&A0.܄d*lZi*M'=Gv[@Xpm^قl?3u/L?=6L%$H)0xW`R;ǚl{O Z R(z[sy#罡źN\ ipYpeemF6W" Hwq,dOSHk4!9X<=MHN_yαMK&!j;whvAoZ'&=ݢ5䒦f6CPstZmӇCRk8y"2\edr R9Iz{LՄn[=/YP;{(NxZS`:C2< cGo=YŘ 6QCWf@(UۇJN$g#V\/JL틳K"abh9A%XE oA(@ʼm~i#5)'Zq4ܲ ~~- k!Ynvp20?/+X pygԭ04Bam@qR?nQ@ 4 CsoS`sU«|Wc~Ίt|̎/4fWn](Q<63BTm[] zcWL0jf38zߔZe]XCN7)S֮<ΫV"j괻lR2܂Gֳ>,<ǛE2, )FVc O! ˗k免KhFobk743' 4~? *#2 r 9'NM69lk4J}pC5ݱ/?S% V.h ۗ_G/q+\=_9$G`ǟzjڒevsLJ=S_;ք wo/{C36n[e) jE4T]yJ<9Ƒh0Z_D1iQL#/?@ضZJ/¿PA.EmN(q'  r⋉GuTCIn|G0s6h`|+ v5tJP N|*1G Qv!hD{:݉/̈ vgCۼA6ƆUuYBJ٤%sL\-LД5ۦ̵4r2+Oq}66Af^pћ͘$O8~(No.Ӧ-6#?3>nq83s]O&=q{.yF Db2?~۬=,6Oa% X=b*鑕/j[ѭ`㋴ Y%ROh]L?%qq\2}/hd00ӂz`Y4p"ؗ S&+}X˙sm +z6mKw<", gZPMo`RMRk(g hW ߝVZ`N({sxߊ'ٳq+ 'r gͽ$UʻľO澸m<_?CέdNQ.Bu/ _vdX,QGu#%O`+V"|I g廏{(Z7_5) 0a\&+(UettH|:S6^/HBKSWK1#: ;KjfIt| ‡cr5ese"C[hAg/B ?k C'&3y?+ t6'{׵2*m98.YK]8$ڰqjE\2K;Q Q`&80ޏM~}ً՜X5)e)㾮` Fmb#3v/9'H Ƒ: aMETc>Y{$Ci Gpߨuo7 ;u4g- ՜ vw$6Pu"a^߈Gr_".kA]2"(w8lp6y`rYo▙ǢҁgҊ9kޝ6I)0aYˌkdÓyJ aFY+i7o[!9; @Qrf]5r4fqD J\|kI3HvZTÛybǸIvmNTwvEDerA%6Fg}KtۦzȪuՕ*0iѰ IE $ߔLďyxvXS)%?l&ӊSw49# :Q0/A5 TaeX *> |4M+d#B-R]j`0=OYZunV[ѩl$זFUئ$[rSWKs ppp =}^Ef.s)~Y {1,p|zo<ݟ#5~+{uߜ.h :suGKac#DE6|XJ=-igY %O)rj{ָ#7vqh!?qL@dhvOdށ1{ 'oXCS݃@eԻK@os~'BT( *'m8YBf)*mP.O`Р%)J #e8Зn㬕Pu#WT`E9C0>}aJz?]-YЂ$,lUPŅ5è/`Z(}w$zÿc{dث{ ot&X@BlP72I^u2|AHRS#rߎ~𴂂(9F*p/}XקJjՋOz2g;s s"ÄPtH I먊-n hT U䞡wYhpr`4Õ1BzJwzwj#~,KźqFGKyLÏ?CJ.|~,NfpCd?#s4\Tou6e@Z€xAmhI@.}Ď"}

CeJHZܧr6yCμmq1JT_\h2LD⨠姤GTv3ʹY,9-9]Z|:@*vdNۖV/*^t`~*%5AbGKN(.bkQĝ*Ray7^u^UL\G;s"q7RҶhcEb@@9$0M#8֎f\Rٰ0?jb">΄GO/OAInl[;R9ńUԢLOQٱve < MӑtBb/_VDXH|[ p OIS=S4Y8~Ꮑ(*RM_g=P䑋8)~ef)*!JS 6M[-IZ%ŲR]Cj'O[)ܤu_'Cpnڧ)5 Ǥ~hiD*QjyvbTz8މDdt-bx9l^5ی y߃ʤ&VP3A8\aU NTg]V~!-jON\gFBYߎl\[ւs'zBѮx$OPW: 0Lw 2F"׿";=!L[&1P4oA2;%{?~dqd(@'{F*rqdٟ f+1Vʲ^*PSn  Xy5cnVY3j9\ wV+X.@؅r"AdKK)\7Z <b"7:mζ?9*SK Snoh`J3B(sQcʧ%aBzgN9l+} K]#mD_y&!vnGj%-GŦbʼn@q<&%eanL!A}q~=Xe1eX[V1Aq0&9B7߄o7WN3H@oZ1̤̘ʹdgBK\T<~ #WF>CP(xNIx'ɲ_}ʿ5 SbG-f"-V֦fʤFŘ.X}?wDLET! BBbm޵ Y'{ESKkW!ݧBgȟCUCF,=3?HUҜhlm6ϼ1~ߨGTZ"e4R@r"6Z-ʒZ_QoP6i#^l l 9fm>މG|>q,h4 ES3ju X:c?}3ŽGQsU3%Զ6ʌW-[95 'ro6i^m DYd a-ap'*$yv}/f{[2$JT5 ${,q@ysd[ǘ?.~g&쉗p NdOMy*YH1]O:+ٜ IgC G>Z8+pZU^a{1iFG@TrLӶaCjzia @Z8=i_t>95 1uO= P)a&T~i; RHp&"ѵ] @ z=sLㆍ(1>/!qÚ"{<|-: eYΙ:'Lc& D}bC\c)eKaiۧȥ\٨q h|N.2W`m~x(y omC38{~"HJ1qLUob `'0msF'H)Ep䰕ZQ1}KY .`Y /hZAuqP#Idp`AZa"gث }K "jF̍N>Ȱ(S:r`ŷJ/00&y|^vv!`w꧓>曢MdJ-I+-,+j3;9,-XbWhӧa~cSsFHC2b @XMX(s-A֪]Q !JRgw->ݹtb osHP:U"@6C!" D,1'es* Xv,a?LpVt1&]W´ߥiZ+=B-yPi"eD6JH!iObօں5݉7Gn֤"h\@qB }je_)ub_Rg_0}'@:.gg)C6ݓ|R k)ӝ^=g{' |WQr*[J ND K/I1f05MrRGf sB=<td[JΠRA6/, /hRq|T@B¯K(}z*b|ÜN7fZ*](M!'12k/$pw0.M`FKj!Ojd>P VAE"c uܬ4:rjbBuo->y`XG$Wbڷ';Up;B/G2a[~zɮNzhyk4{y} +/StF & ~*}ʁS^Q'8Գ'ኤh3zh Zvu-GEՎV>lJz)~ԃcJ^3%]?f$#ZEǃ`~] X~ GF^Hc['8 xǶ3Bg}Sr'zEt5=Ls?3(v8r_$ *쳫N%#( k7a@RJuCȢIQ04{$y7ޛc#:f9PH“e}lnJ*&D=4# ICL<֮褆y-Itvߔ}T72Y8R(izIL[:8co=ryC), s lwuǤ7\2=XDGRO.y%I{;GK$bJ 'jڛ̙tyEcׯ$ xDZ/SM% n?RnP_WO4/b`e9wro!e~"#~@cK+r fLک7aR_~ɣ!Zv\Ob W|H\q Q@`c2d>|6Tֽ}'"mG%\fȉDQ9> |d`fwtPsuns0Әe+-zqd=[ ;aaݯJ]DM1(Y-i~n~\6{._^AyQD=J3;fC B6"x]v1Wˏu9]>b c=kȒHv*w(_hH~(xt /ۅa@7#dE\'7h V=H~mu;֘j̀̾SZlzkK/-!.݅xIƄLKkF[ՕPi 8&Qu.?\< j3$N'BOaƨ/Rj_;Z8 CS{y<KH|f\ r.bՌnbgDCZ?4 ?Pc_8uQٶns(u9+8tqAPֱ'f`/Y^c% i`NtaݳbƖHlgIp~FoUY4VZ@m1t=~TS:.l?hjX<׎j =beɀ:.44 8&ZC'a_L4d^3vԘGr U+o!o~%EںY:N4- 잞; SȆŰ;~B&hJ*3L*|0bz:(F c}k*{)2O28{˓aD:>ؚQIt߭)[y=\oߘa2t 겢*y{)J(]Xifwؼ@kN2蔅YJ!ɼ) Q ';d{!{>2trUI+;яjFB 窦AP)ׇFU-S>>ۗpaP9 ZonlpNx|}nC(hCѰ-_3ϑguk,;YN q8=0dGalF"m'XG`I=t_Gn䪬4һdj}B`Qt k+Qm=<0O6&~[MQ -%R㿠țm`R,Pw?V;U ҟϚǗdh/p q^H ox,Pr pswTIbVA6Q/yw[:t~S}#=-\Xkri=pi|V}-32U@K/kƶG"Ղ̹<=J&Yڳsa[/9(H $ j, Τ~y_u*Y@2Q"0yO]+)L]PՋM)ݔו?9 L}jHhA؂"䯮"nx yDt,o߷YCȮr l((;} ['^(eiq3@rכ! ~}i2rA`,}" >J;xSTVdk?&1@\Uk~?3:IOIT2*P_" Y/ڤKZWDú= Z2\e,ܪy6o{ -3=*#iF3859y$ʎۈ]Xi?F}6fI!S kY֔tjo {,$=?-Q.ARH4 P.7Vd?FqC7Lsn0$,P3ټFW?q/Qv  XH]w6*b۩RǍn]^ y( =ꔸҧZ)n&ګ#ۚ:Uի2#@~S?T, p\!A>-mp\NӴ5p\]EbbζwP1>Yq:߄_iY|&$R B]mAΑp"5G$Zk"-;W[J7865ğ[6ݦ"~],OT5 ו@MWS bʀ)kig"ڷ*$Hr)DD/)=Fu58}0m # NU*>HSSytmu45X.@dP?28%Vb {D H*\%˙ 7 /1|QsT&^"`Tc&@s<& d"hڙ-9OAQ$i\Uէj}2#vg+*(7cШCT6:"Z8X6N)@ b"ɏ4_3uȈ+5)T q8x;Pi zjHx'T~:RyMJ%EۙU]}֑Ym8 ʐw}*2ka|*`"G ͋:)pEu1.l%Wx X :`9ƺƅ0f?9MII hVG)\]p΁?r3r$2k2i% R=9Ma[xK^/=@&Kbse,+]H,nzrB߼".#ݘ`ԙ"qAVU(Ypg?=ٯ \f;Сqj)t<3KhGVQN&<, \⽨_vL؄q@Abh"dhF)^\`;y*LEYP{'gԕg{ro+;JZ09\WW|/0Ґd:iNi,rCb&jAJrt-^Uqǝ1![e :e1˸FI't67j:rBޓwQ9AS yz%#cd06_rAWʹ{T,f 4Hd&9>TUG6Ղ䂧;-Oj^J+c@H%n7U_ĽY*VkޮC9|΃f]& etb*ǣc1#nm;;1SI֢-n-]do+] N5xep:fqmF62()_'czǂȈaV^b!cLDZHSs_X38ZU4f->Eֺx`$/Vs{큸 ,="U˅0^|eEE-Rui4|x^J'xH2vOj9',R/liR/Co?7䮨% 8 U"+8y][ʱ緲s j%H+詪"Pg+qOjPdBc?R 6jDۣgN,l#p_M#Е%f8fB_-MŷktjEI8A,hY"sU aR:QɎID˸ȹ\G!3pέu"Ћ0T]0S藠R%Be1LDUYZpn) 9#[/:5^pT6vY78JEK4xU@rQG8x[z\ XFPrϱ&J&]X/ID֬MaȘ0cuY!zy)7g3MM(rg'4L>\J!n(>'Yq`&aPd墱 .YS֝ #6ws":\ |$}‰LmW2<{[ A,4 Җb:b2d @:DþQNW9`ngoQ ~(0*.vF>Kш.pBÏ DYZFA陶9vmm.?nt46Ӎ ڡ7i}p" .6k+$8A‚qY,?wH*z|(b, PASoicBKDMQ>q+Pwh9׷RCЎ[GʰJR bl4מTBܦ0Teu IAb֢ 0P$~΍+=1`>H>e?^um[4/_Q="ctG]3.:I.rm`**3PO3ʃ.L%/闲J`/bq$驷# I͇ %2p`1ȠF]WkReE')Fw^ieD~ 36DmFE5||#e?O(-uCQ0HbHDs_^Ev2uwP QMw [mu/uLj7ćϏɴInWiVˈڤ-Iq; /Cp<#+ !^G3J֋fr,} PO3^t*{miggY'88'GXŲ{ 0R\}:k1,[%_sȍ2⸇+ʼ Xa_{Y恗{*&#Al]f[OkԄU4P-u%>KH<x9ù218:oɵBVM$ C + p}`6dJ忕\>Qk.qG}@_tl#Lo,9r&B-BlN~5Y7C~[`+=٣6p^XP Χq>]he/1xhza3u_ Sd ^) ..Juf'DpX~}[߱"]`T.mѽD ,|dQ$o qh6̕>n qMe%.:olS o=3 BFqmI} DY0g=dVE qؓ|xYNBUa-DfSVzY.fiqM񆉩|hcb)zw4okX)Mػ|<*>&`:0VfLA|\mG}Cd2o )AWg^M!upwvY!ffFjt?fLlz'n/ }?Rkr6;y+)HwBaDcN''"<Ϗ}63[&^a`(RV,*@&> k8`d: soFkuEܤE>J$VstN^y$Gt RN| fLINYgV\_/쓤Z( }P5/ ,pjw\yH83dU ?_~ CfXժXG- osϥ^ StڇV&r@'ZRg#+JQ>yH6(09`Gs#!wMA45]t'wU\1xx @mV=F:s ^ĥ;l_TB'2,a.[M_$=9hjxve}!2f*1r~fd O]OAL9oL@Y7G jTBiK %s5,ҹ*jkEķ ޅ/?쓶Z\A6`}Nr* X37v὜5 +%U +'Þ?}[FW%6xBz6V'@pGa:ZJH86UV'<_}o?ucQ_i^_tk%OC1mϥ2nf:5Iu@[t"*⨵)"^JkRhvl}dT\Vx) |qNXVvŶJcwΰXO^ؚG%~$O(~|%y m1g8g;ǚ/ CfPҊ)&MeI=ݕZeGZN`=Z=SI/!^EwFLxw. ( I IBBIk{ܨb &ӭI=wE 3J.Dq|^Xf> ?nC=hԿ`م|MIm!Wg{hy')8zeF>d؃l&V^jkepJ!8?s m4y.'+oZaA5&Pou5qCz(G4Ă2+}z[6;蘢O T@尲 "[F/ 'ȌMǻ j]%qmCH!|G1&`99 k l),1x |5Td d)DݨE؀5?vt K!Wpt$]Oޱ{U[LC{J:NAm?)W}ъ|w!I] [U{SE ~?v/Td8QW\]O-CPNK6e9OtC͐S 9誡#=jk풓)Tv|k,]&ZM):l̄?>uGc7 hμ=ǻ/f6|0Sʿ5#ԅ_," DKTe#xNt'vfARLSֳpϏQUpZx_ӡ[t<PD|^twKܠ*98D ? ތ%*M& eYC6i8-M!כb#թɠӌFewkwtS(MQ\)oBlfR|/ M\t%&T5!j(nM=Yw?+judOs9b[}2pZdɹvv'@ణ'E f^J_Dt߅< zq +to4H;$\SUU|XXXʦ3M f!?7;AUq\_f2 <}*߬YZ/`0Sk{&~ͻ\ efQBdžCT.Vl˳Sv; q|*Ҙdɑ=%9V$@Vw)ov}PUY  dsۣL4*4As& :7FE5 =n >~N̆gis'9aSv<?5vywS2$:_xie " Ќ@fY;_e^g6XzȊ~8RR~:12_x&yj>т&(wǒ5 ;CYsަ[.Tpm+Y߹0/pʋ$BnEM膯UzoZȡ e QK]0Nl-?zE=:BPKk6A/=/N;WQ[A7RRݘanZ *sR'Nm76Pҗ O dIODV@zi(.*{Cb6duq#71~P(zc+wۿh܏ݶ~zEkV&G"fp1iEY$\ q1dH^SЍ~u{rj߿dsJƮm&)RLqq}532˳=/lqcR أY66FF30VSh|vcP%N7* SRF0 뾯ɰ'Vzst/Ε̌Jqg fn#xתy: geHaX+x>P.7vuܝ&N *]OʅS^Z H"48H<ƅ9l poH5`E ɕ`kfnbAUMn;3$ܑk']WF2총_:xiVxS8$Ċ+rd]Uz֚K:5BBOVgA؍ǮҒJU翶*ލֽ[>\%J,?dBYSweMwP>|;,^606}TvsfSB %;ߨ&q#fPlAUhri^ ԫq5=R:W?!ꕧN[4MgZV~Q K'. D4%-E\1娿kE Bzod~+J x#OM*F D-T%cJYB$ 4HoR3u^ifm8W&QZ>$ ;%1yh)e|كDf PKp(AE|"J&T5 Å_@ҔGLbb=V;fR7N'qCT"/y6 =Gwa5z{AL0^g#zی]M">DԼF''%K|*p k /]1I јfzC5".sODq VY@.^C7 n@b2ڼ[R,t;&sej?-C d!G!{4ShV; ANȹ@dZ OЯ nKe8R4yzj?|%gFs ^>fܬ}B ɦn6H+B@H Wmؤ} ll:Q#ӝSk\̀lC^;Y"zHrJ Ёv *bzS :#\ (a)쾊FP<ٺ:_T_^q≃Sc@_L;~s``5䈦D_b,nB5aLl=xT,W}=Y02g/]C'a@v%?lo';}D3DIm$Yˌc&VR7 P؞EI<$31e`Q.мǹ:R8U{!E^^!h)֔XƁ#dJb+KuŞܑv:g <?$Wh[5<|Q Y VC&i7:!}@i)Ees8oNaN"FjeXܴL36\ 궍.iEU?n7Pҧt"ʅRڎbˉ 7Z+ko>7$/i kyFD JaX6\+ 6FGZͅ Ylkɾ޼t-A'7)Ql*l^sk޷%aJVOGz,q'66s.btac~#NIJ9\(lzP`-1NySVwBtjO`@Zݙ4_9XcO(8>~!CܵhkZwb%B-Z8h~;EޅN1kܫ%^x #3[XV]haOϜSB^njMPB}+jw($Q}m=7?7H-zmͻ \9$$>ɮ`'}̘ߵ)S~v):jjV=N8YIҤ%Ql|@'AAs 2` _$S 7 0 |<ֈ^=QOMvI՜WƖ4NgE3*).+ uRf9q, $*USvd1,凜Yn?P(5UD:%0GHZ*nfBCܒVSd|րɩ^AR)}l)Pw!iy~jb1NR-lLg!UU(MҿKM\\p'}zȫ!] _$G=O 3 vANJhNs%P0L/E!F ;m:sPl5ݺNjtT廘L9Qb m oXG fΚ]#ꥑ<8#*|Y\nh|Ww!{Qh8̵l}a6@t@(((82xf?ݱ@0\ RZׄL5+l<,w-;"z$5Q6k1fpAym,w0M`#$wzd~\=JKI5n|ALo~%,O*NG(Ql  (AɴO$=|ϗYB>!΃+S೟*dVloӬEn5]A10QuC* ۿ%a5OǨAM`;6`Nng!_=|Q f7'-ow~Km%&+9l6h*-DD>PJ&BϬXEE.# ;i~ER6LL4d;h'ͯ.Oߍ@7ߜdHI.Z4xÖ&vJu? Q͋_?@rdG44'9:'"Ds 0iX6; xթ3վLuLo>f i4>1_臁bfFȻVǟ.  /j>0gT,iy;V9;'2+<~ޏjuX;efűu賷1|3&na^[U#Z?yK ~q2<'&nDf h Y%}XR7.DG)1\"v4w:o2Sļ|=HHL+Ճ4Ŋyt.L).RN|<0Ӵ3 /M`wEa|CzncivnY>AZf<\;9MXaEXmrD#.XAHLj~TGn^_E`I;:o@dҘ~mV#o84~"d!9a2R z0"X؁# t]lꮲ4ͬST p rEШEJDk9*>s q>EQnRiv/.\dnq6ZT2FΣ~Nٿw3|[AGX>;/lƆj03V"b(87kWhS{x 6Pc1h?;ڋ @FbPS{x~=2(U+& &i!Yo79[ivCwmq5WENcb[W^տ縮P"pc"1T77K56*Ly<Bƌ BW[d/miT,u͔z6W&21N΃|飚nN lVR+ˎdYLqlڙ-4bU${;BzzAA?sjh~kB}FgqD!o:8(]$,ll`Qǁ:JhKI.\*Im9 F dQc14 &5RY'~{ Fͩ",*; " Jo<̈́w""A&L:7l jʝ^P:qd^t:jRR<}֞ruUrnށ?NBo=P{C7#YyvT28ydV,+({@J\Z;J}XPVN@@yPP_(=RL<~ԙ n=A_ :Y_/ +o,MxRN:ce FZ.Pz 3Z%N!'(/|8; %'FA뮄lt؜h@06=^b%z!R4Ə0$:ɾCV[1ahrI7^7 lˊ5iM-X 2"1NU3;60A)G#ulfJ_XnDy\8fouz4SX5^w'>O@Ț2d^A85vQ!]פ|oujE7Wr5E?RMVyvp"|vppLyH[Dl:Z^g٤(8/w3I`gu@-_6yRnxU aSco- E߃ @q 6B?m0;⮍oD$0747uTRxKǨM v8 8'IԐڳ|Z +GyĒ]ynph 6*BHgz6,NǖGJ7wÎD>о;')[2r=v8GY'_Z%c6MPJ5P'7:R7 1+|*}'\Cx ,#m-F\{Y,k<WC(F5/or*v!Uh//.9wsqW|}#P鵇[(o3O_paP|StKJm xsnDZu$oկ@Q<ObiOONʬ3C7ռouӉ:0%ˍ% 뺃YE%1Q =L+egP5S;H҄2$utW:h(llK+feZ>W騯S,A~u惴o]cJڠۺCԿ5VHk [&Fn`|HE c" !ޙrB"nE`^|[XH.'8$]a[%ϼ)+i;g#wI7gOl<SECsaiJWVSw5gTh% H ]wE?kM{;\xcqAnG >G\`ww`{9N%DEZPfG? " WcUq9yR:^$T#Hϊ7蜛/לF4PtOuv"q k{A%h ŏ0dbjz4 FVJK E!00:%W1 (j!7/ɖg判?>  ?VRV.o|y6 XZ*=pj#wVIDŒj`z%fM)bCnƻE_15LrJoL7.$FTy>^xECXt5lq(HNTq{[C)i_j<˘K0);IrgV0u2qA?P#aGa,6K̰ѲxJ}>XZJ e !G,@~W==E9PA[[B ZQqd OEF|Qo Ż/s=WYDc[\SV7bmإU3D\[ڨx@eLg#*ꤜ]RKRK)a|fe$Q"9(wbdGw%!{wtd gjY}k̇׺{ICY3߉)'9y(KJzݦ%^8P_8Ϛ`|[8..^pËyU,:h7QbrG1ʋު+"x 6 v. Ie |(x*}X-|oxQ 0k֋,U'lWF~(6-ۉ㲉׷`3 gcיw佰\]h,♘\ep$8@J$KZ'ȥ<U֬?㚎eˑeXh 46d&5b[#[SV%sVcU٢۳8o; y^3 O[ xq6&Z~ARp$"5|$W'埅ċV5@\ ŰnqF:/s=g.hMf݃"rpn6=#DQL13:mz0^'H} F_du#,+IhPjj~<.oK7%ڃ͝,/lB=WiyݨtJt(l:d2yF+D[WwQs/[ڼrNή`+s 6y7NeCN=lܩ־窶%4ǭy0(ω-3nؘ#F`ʀ+8F,Qd3-N&2(>5IY[h'rxS:r '&gi9j؊zP(潟rv +l2܆*{ɦ>5Aown 8VUQ;{K"릉ojWlc(_ 1, e'2FQ QyΗ{S"iG8jdjp+nxz3(·h=+>h9̧?~g@8!%*Tڶ==ӝ8%^sY/5ˤm,A*f֌]7'378 l)C5SGfZfE-0+ 8>0E48p}HfM]"1x 9t /]< ߕ{ae([wnpDaw:AZݴ"!xvx@iP:v:QũlBRMm>T7G'yE`?| nFBkwWCeqY]Lj%:+Kǀvtp6c{F)o/ž4H,7~aI( w X߲v 2aB[}h[hzPL?e=e9ԕUP0-.LamU svzFPXv$^r,׫Js$']1ryOxP^!z6>3mIIGЦZ߲Mw0EÐR=wvHd \@&͎ S^*t8 LH4M*&{9AHyj<luޗ/F}NF޳ ZP"A?xVB Uӏۧj!<[n͔HoNHU ڮ[WB  iVY1k3 L˾e2_ɜĚz6n2;Fo/RuWa#;P,hyʕc~ۮ!B`NCtyo۫g!z%7qY&䝪,8ɛJh;*~TT1@&<1hudR>B3#@Ѐ}t_T296 ) +d@EѲ9L <--6L;!oA+?L]zY<9H?\{$?FQ$I_ '~O};Q_m'"μf A;&,7Lui e'Q^i9V-o6Ewl.x9@$y!an}uWwHt85D4B ?pғ%ve8/SlokmDPvzon[7o*ClpC~S1L(Np=Op1Oy?8?=>g@9mOo@a]pB.[łlJw$/kLnMkj}̪08v*eɝ;(Z1qoV)q$3rom1ZwDBWA䣈YW $z8hiz G*Eg %Wؿ[ Keu=eIwddqB cmFNL+enxb9 vf iJH DJ>(&SA@cCꩱpĮ Cq#TJi{eeژpr[20$k6!k;]5> m0v4l:ǿlrڗTH{b̟)$woOP`#U0 DM7ilx]*)B~ʷ_[H TnW@zX^7J[K\%N9;1İٳǞ2 :GgWũҴ.E0c]e޴Mcbch9fG21Ы+p'_L{g5 OFdSSZչgAUZ>)*pB}B$g3:kl gOVן"]ËC%d2FaS23]7}2lmC,poMu~#E~F^bK>2=@vzzw } I`S\gN @> \(y&S,N%BÒ~K:9L0hћʃLePYNW\!>:$Y6'#q0mt'?ޔ"a'wra-Ea=0wJ(3#$/c"nAgwavRD%)f[`Χ[Ev-W/Nb-UUUyvʨyML_[~g-0e4g^$RN1#7jNsYK;䲈kcPʨj9(Nqbts~'h1nNZX<(|l{[֎0~gUL s-#j\8yd[9"@'͵ ]APn}G߾ĭ#)H~nc`+"X.P aſUC\õ?m7D9F٘0QtL,(^u;&Ҹ p (!xЇ1eH2Hv9B.,mnA|SuWZEqv(;HW}AI&hVya;֨2(c?뤛gv#) 0о VwƂ#\֎,wC][Prn3Vͼc*/FSL HI15Wξ]LضG瘈7|+ۜh|1.?L]_tz ~)x|?J6)>ET[' q0ߢ$h1~/+ KVw}dt|/)ѬMrA ?#vtZrբ&z3KԧδE4SUtŋ1*3"JjMcvѽ079) ށpi. 2#USR $1%+O1 {7ÊΥPόG;r#GD؀Hb  2w`?LlUHR$zZfRt)PAc-fp& }9T *6apM#7C~ nTQB]"ZA7zwS[LgHDgK{k;?ݻixua3gg"HYZM&|Z9mV9#QzDKm-ś1Kr<1QW+I Tr SC:? ~&2 s $fþm⎅2FOg>~h&%  ]wKQ$̊@6f%NJ Yn2j{t/ ){fYf0;5:N+b}{eE1=RИ0v_;JwikU242>Jm߸O)y2D&-xOE%:AAYU5ީYh &ks?I\s4M$?a\Tsvy鵏}5/AᄌQ 'A*&E*`MKØcOJ /@E!||1vXsc[9uY%#LO*3vWx^@"=3-7DAsQ@,Psa' 8>"_qLX7D0mRH|lodvjAnMvUsv;qYʛ GfX ]c"2ɹk]&\]()'Av.LZp{= 0}gyFUgB{{C2;KlzgeM#$]RRB0*ePW%%5HI [uGgw0 ڟsTb2t:ѽ ~q>G# =VD:΀WI҇5J8jh SkQс)r~?sW>W/Fl@ %42G\_Du˩)>VhY?\A2[AR(-eҚ,ƗF ^/` f?}{T_08M;>DekՆ/rvc~i@}R`J>pR\e[ukX&Rv yn RtoƓ a`M!$#=2Wl4O~e|MXL:Q8Q1= y=xЅi􊀝zLewG~ E}ψrfگhT& QޒxX@!CCպv+߲ A֐s`Q⍞8EH;Z @ʎKFIygNfH$0I2H2lXaXI~ 5|a#*f4crIN;]Hg)3E%({!USw'kӳ;e{Q>Sl,ۃw-Fӡ#S%٨4hSƫӜ竔%i0o@)^K FՌznV Rp'/Oy7矑]D k\@WCYxC}.tY_/~v[[)Ux 4e>L,ƭ*e{{ H=G_wy&,lr\ 1Jj/b#webJT겐-f:;7-/,+N scj#[hK0rn%#fkͷcU;}dq&| q/O"'2ۆKBZ1A ӥ!$w)=b Fv꺼bN597TX#r:Ԟa/t? ʞ8[t<(9§RZP1DMVp*497E݀"Thu8cRUw-{:5|=zlapSZ"/f2"KOe|siX<D!h(j +5\mK4Ǻg@z[9FL79/@C=Ӟ;?mpwC aNC8&v?12ш]-沠RG7mIC}K4ϙ`" /fN>[1'NV,`un'Tު4p&oǁ>!,8T }&=jh#/YBjufЙ,vY,c`-V[Q =07v~޸9a5>aLFrDq@5=LnР6+[ 9ooN ZcgYuXtM>Zl1Ԩk(hu3lSjgѥ@+AzIc%5LWFqfмC1|+gzEYYDG@Wl?]2Ԅfr]ZB *RUmq=tTF1u$GyLڈl68{p/ڹ.͆Sd5Mbt)DD-]֑مa&st3&NZY^t-_qEW~Ǟ(_i(zr^@oU(@: 5! .CoZGtƊ' ow6$sj,{1*Kݟ7{VbO켂b5sye`{ꦿ5{eðł˄22em:ˣ PtMEه Cɣ>V_>~ym`ڨpfv`@ SO5zJQC誱/+eT@u;gou[ јeԟ OO 4@c0%{OYFde*qO(\[zuR)o;lu?/jwL&zC"B A_-\zUj@#ӯ}ْC^GO>e'l]ڤXve2 `4q6 n;,RUb~r?omTGn_)cacӫ'9q 2BG7]a;xÆV43r&n}).֜(oT(,|Z(@jr+WcD>mT+f hz>8vWķ{ ?Ζiٝ DvoC_!N@Ag5=~MC5cKroguT\,j ڮ>iGDRa,skaRC ustVdC"IL:}g! k^N,~(܇@goүS"K?K@Ct`fŠ9A }*I\o$gଶxsw)d&m\.( CGJU5mn=2^YG.R[;3t?~Lއh~KI9rLZ9G*ɞ% e^% $5/'0J7/]CG7&B mx=$5LLQ^7.֣k"K:`N88k!ITWmk!tXV?r ]NdS2x׾]&kxSm2[ Tc5uKMӳJ7{iIT޼= tu4X Ptk<Qii 3H|lQT]U,_dV=yzo>Ag"M5ȫ~b%"nBIؓw;f[n\1炗ўGt(l L-1qrf3Btu RU`cPtQWאΆG%$8LxJzr"'<(6Le$0)z% 4/%Ym} ?=J4.g fedVpWWkng{ DIBY'훝"AJOI+MlM=};"/nQ:| o${nN_.=,I!/^ l7&Ea"u,E-خ #S+U=!'M&z XPt&k5j ^l,@eCܔf%b9ʬf̷=9.hm>/ZWtP-J Jcc О8>mii;T:1Rkδd+`vj%\@˘譿 r6r?M|\|͖Mi$||mZcҞ\BzURoeͬx YYni:؈A2315Zw.5yB+ `BZ]l &gy0@;W J Y=M>(fO}qXgŖfmNRd-7vPF3d!W3[ Qp& ^ \cp R;8V|)4oxWwM}`7Wc4bqɁh.k>oͳ.Rttެ)8p+\UZɬK1rXk⋐=#&0 9tv03d77ҙlca+/ Α^hߵaJ7>L6C`kv ۉY3>r' )a?@`(HD=*9dD%D|b}ƟtTjNaPS~+(0_nv eSOi*Dǃ +=)\aU!U.Ilw>goF&Wx21$PmFpq춗@rJDxXm3 N`{G>bY7g/?S8 肒wufT4F*dbٺV{4a 1ޏ8k#05OlCݎ|tYdD;Oot!k-qk-:L}[3k th9RlO"\Jt9ğ>hw7^2qkIv M tfJEZVp|  aa}5[~sgV'[;FrZJ>%|{R}HU}.Lh6{o葖 n+fnv,r_êPX'9uҀu'Q9ab^Uv@bzGX `~~Rǔbrwb`) _iƳ(tc0b5׭=03;G2aʅ:z ^d+sorӄ>p' *N5F, s Mn "P=qJwZPƧџ ʀ r7c?G9jyITE2unA|wE)BbəzEb"s2JR/W7:P)Rg ˳[͎$[b~7]&OÞQI_M5?cH<7;7d`e=2I :u4d(hQX!U܃/HMm#0O{]E߬Lvprp8eԘD3|q_,/ d{pQΧt+('w@27lނ%/Vlp)9 fGE np1Eə<ƖK? Xkad6m+N{Wd=\zeWze IisZ"TpXs*sīFslHҤ(xv2_Td~6 "Oig̯8c>"JTm8V.}zire:gfڃ2㘩$Y#XTjCקvn{F]wϱ[8v5z( cpl*sdAe BNvQ!Rxf"ʰ&qmPx9iMuc$IŜ?:i;C.TGɖ c/L,WA@t+].﯎ қ5'>($\H_X =Qx1/i(wEcKӘ>`1Qq& wA*S |J-KNTLS4 whMQh5]nʯRXY(iGcϢޫX{( U}p@8>ܼ'i F.qyKA @bnLL⴮(ٻnq >Cj .@yVwWj&gެk((T:e~:c՜rVT(' ;j_x@Ƃ~ͬIϣ 4Nja19M6܊-n)[^A tSbZUܫ^ fkx4@0p_#3DCi)V6gr7P:va$M~LzwoƒNLc_XE)4io \Uir^wBpUǸ[{&; ϑ%SnQNH*d.=@p*=ل>YW:3-="I)/ WJ0ebr*N#*|=`x;лS/EYJXNmpI[Pj{ό]d ]PN$"v=J܊RUl:uтDW~mPe!y/y,8Uʻ!i/m+O, "YIz5ά88]c>BJ*=386 )Y*j<ßHP}}^pІ9bk'͝v}ê_foInW#TSB]&JHpT#hҲ<!MId{LQ} zì6 8QE[6X Ri[y$ӏ:{ې6]'i:ƷA>X#h~ybqVDoOqweiљQ4_1Vʜ\wOoc,,W5 xlmyI=Rh?+ ..-sJ!Dzi2$f\X\ŋXgkhVo4-UHߨar^8K$fǰ~]w%deTf~&YธR6ivgzץ%#F7xm͖]t> ۀ(܇z0ο\w0/X m? wPs'nYq,AME&ެ-Vzyl(:7tȮ"5hzXT;XmLGp\/&yE!aۅ $Џ`qـQ<ɪΚqdm䲡03Y<=jdWg*JХdL4 p=|źD`#NKޏʧx̷ ۦ`UH;0Ͼ!omZ8N G݌^pCp K9ӛ\DdDK{^~h">CGZ:6 A*‘*#*Ψ$$$0/qՅfv}r+mOg)N[;$Ϻ)vr=!uARNa/vYΔ}XKnqR%JoCC eQmyKmf$;_xWZakirdi @=AyHFd%ɤ-b]T& 2YBl=/"ec_$/`5xSSMMk9ƨV_G]I!n$Ox;f:~`p %t6@y`MT&i@`H]am79b;'b1Cn}„A`!$Z _IbC/5a v[R{$o72c B 2mm4dҵd ГRnqv̴J9VTS0 }ij9 h)Hq]{.Z~eU#'! S"vǥCۏFT,k&j̞;DO=BӰcs򖱤nȱ$lP>r\"?tIL(<禗6w?|]%mŃX&>Ǵ ȴ&Al׿«,EC3ɇa4V<0Wz }pȇ\) Y- 5Zd z C6ݕ_4|]A͐6cg3t! ?IdpN&1A.!#2"< lW?llŝ ۡ\]!S뙓}L$:Yx I/W:0J0}%7Tꧧ$;X 79iFqC2chaq(eP۶S/c SJJjmK4ih g#4)KR,{n.% lЈ.Üu h֙E[P0PKdXer`j efT"9L@U!1Ή5euo/U.i rlJP^./Dȟ:7*rv>n-MI//kc7bnѥN-DXeXJL\Ox4E#٧͛&oKd(`YS a{yn|Ze@"@:R", N~ϧastS3  F@  }'  (v0CNqT׳/&sUMg!vn^1G4RTtIG8[ 2hʕ `?ٙ+#cyNz-a{"46FX"㩨!uaD[GJ{.aqc{x4TUjzDZZ~|OHyW1e@|J._s'[#HZΒC >Qy _J /Hۥ]a\rQo*y 4iKjd:.m4*hH"Q P%V#4NszV{jt $Do\@UGKUi7wosInSV)<^~/O8[$vmec|2q10/ߔ_/Gg$/Ż=@2\ko*Ҩ&3<]NV~FQXwm Wa.P ["p\=tIj`yWmǃyQP};6@Jc OU,Aa;sZ( o*3&Gqm'h @$^ֺ ]?ձWNJ̩T,7]}cs>%֐HʽBK` `ᦿ:|t.3[ڌ^[:tÅ!eY.yk(vf;![eDo"C-9?W(6/)Hl3'8xGiUK6m:WS-h^vYZO4=Bҷ z;v#9` IWqI?K8{Qa @oQZ'^0N%Ъ͗ wz #U-?*OEK7]s,"ăljݏyBUJþ.Jd觸8Aï⥎z 0Pz^D CQ/MmOZY[Yq?-`|#aРT`s'(ЂƲ/׊0Ǚ#W65p>|?c o𹨘p\Os8"jKj+'2Չ49ާJMW/WM= L OGr]XefY<8uD E]:C{ʉn|KUJ4*>dN7.͉}Qv3aN*5ýQ)6tpUWgB@)tmF}(A.oJlS]>1_@]?agc-Yt)L|&3Adtd4k5Y>hnrcD)֦w޷bd {j٠,!Ӊ{(J靵i[_ ˶PN'@<# ]U?W#?S!AaAqekCg@ĮUBP *x$ {^ArX-ásI4+ lDRg9CLQ :*7Cl$GO7R9=b^vxOQ?7\6`8$F>L"/69Wͬ;U[ٮ8{2f"WMh9 7 {(o'9c>=KqdN29/O :L1 kվt[ńH+V4P50E8z 0u)}tT'KtX[\Qc+O{Hf#gXrb=.B 7(8mmI_x+>+_?qOX(";``2l &e 50py_焲}@/߀԰3>XjNa (sD{IW Jw}ηU_Yz#[T7?Y["GV8nsI^NU-r)_u}sM:D=;k7w~CfU !ˀ9mAr=_\Ma"*Ӡ1:mj*_YjF%tQZ|x/rxav@OA@<|g3(LW<-!Z1W@fse.v'gx[XA.#Lmߴ 2QD;ţJ]p:mL㏸ ,34SyoX ֱ&.Ғ6Zmf7 ZfsSs}:ƌv}ҏ.TUh}xeQL[*DJ\隼pb/]@BA:cpI c_"Ci~̫g' tqVDf57Χ; 4]wv:Vz3i`Gnċ &T(EFV&-f*)dOH_KeQVtuO ߾ʒ"E9`|Jjsz9ݟϣZ^ӹs5c^&(V|Exce㭀Z]N"?Bm+UB~dBv|-Q/Ԇq3ؿPE֌FOYi/ oɽhh/ 7o8$Xڋԗ_l̖W]eMH sXW hTKZB4cI[:??'dZ֥utéX8->0r-k%cyh9dvWpY&MC!"'"OVt %Tm]dltʖ^S6G~l U8 !~@(75qԋ ̩7iYL\wcl~/x'زzFo,xR3inЄHѿ{P!jK:Z+)yӏo;@ٟ2 +=*{2bnrӡ/Res$h(p? DB\76HZ#kadŒ܌.P&G4֝E ȳ8wX i/\NQ{.JoeuJU0'qGXPUeYFb-vM8*S`^3MD9Sv;YX\b7L^AsUu|X$iђ kc]tm6 ǥTMx8u̙?Jĵ78sn ʔj< 32P=o(rM&j+!Qݬ'vWuj#s#;mqy+G;'qX&3D8ZghQ(#8'瘞E"nJ\T\O&ۈﭠjW>XTJ@c^͉ 4' Ճ)Q"@E-- d˶rnfw[AgI=r(cfJweDL˴ŎsihWs8נŐxHN܀#,}ɯ0(khx g~==o֌J:i\ȍM9nJM1]@^m(3$ %f 8QI3\= 4 vTJ.y=WTO2v|Q܄o<'1TsRq fe#~ KW^6l'ZϖzO ;UO3v#xgWU>y^ɜp0>}?+SZ~]+HuFP\ D~ckƯ;t1#] 8 紫e仐L!^S'z8O=>xcKa싓zPD'HTC|{ul3IbzpU-enN-ÉDBa?@%Idѻhut1K:&A#2L}1u=//Wr :<|YM|N4X,~]\ __N#cWOhdOgXX^IrA"uVDN\lmb+a3 .dZ(EJ *IH%o0+]:@ŵHPΌSQ",lnYQԻhU[Ǔ_ՐUߵ9 .<,GvEX {cA<Qo=Mo45FMP䛚I zdy'RKH,w~ׯ<',lnR_I" % kP2F+ BGZ 2㒎:VSԃ.?ҸD&pFu SnT}Cl@H ."ɄAl.,A-C~S,2ḩN[6q0)y;M*&c b(]tjbf]1b>!N7u4ր [Gy>. )ύ@!Føͤ!h}nV+h7-yRZ߹mQߍj*IC'u<8t~p&EnR᝸|/A,!6C5x",F5D/"S0&-i Pߡ5n@8 Z|Z͊&ZrYR:Vr68^7@ FLJO0@9s~Ł=94 U2tO4Y2v Er0wLfbՆQj9@PzoTG.m~#98|\u4@D, #Ώވ [k"'Iz3NLdrdvXcXxSï,Z& C$iQXs$S.ɩHm-oGA/U*!s|]O}-d- ~k;e"%A;4Fk Ǫ6N~ZIux*HZ6]fL-U!Pc FE0 |Qg9STc_m^뜙.եA7}aF:Ew'eEf\ĈYm3#<:*SqAWn௃Ho <'x0&sPRAU T3/QZ!ԪKTir~Ān=JpjZy-M&pqF _MNkY2=2.h,ߍ`NDz04֕GBW$4u_=yzZY rH3| ybĉx|0eJ4/'Ƞ|PZz8Z;3ɩh4ht`ʯvhIRjB?sd2|ֹ͈\ M6v{EvHT,hsԉzWC:8KDks!Ku%бIW|"޳.!u Z4s)_7,Od?NB:y]yGmy5ɪ3FfCI4V9{j3GTz-MYMFM۱T%aX~@ GPTkLi!Ta&lJ MB.ؙ%Yivrw^d6˴Q#8ثԞՀd $F19D< Rc9O 1!@qz DNcл׿M}ijR4FȘ)w `cvn)/#Xe$] ˜e[pX s屺Yy-Fe T'ǽq%d0 LކQs6;]0}`17tuW1jQHPfylN.XZ "Ĕ%Jɴ=ӏ]T' M5]OpOE!jj'ı(f=mn2O#`a,H!-['&X(XXTHwd 5g!Qr3'{Z36jt:&a> Hҕcp_mE{$yX Pyߒ֪>^#_do/)-kPo u3`}iΏ@qkXLW.JYJ\0 KE0LK #W!sLIlc Y$:5^ tN)}c4^.om/ Y$;$YcB:"a3*v%r}ۼU 8E*a2#uw9[Ӑ O:+_5vl oK|T9p\Zlbu孍¸L)C[){{8^}}޽ 3Úc2!ZgyU`2I`"mkzC(̨ߺVtc ks?%%sw,Ψu;D\9̕q K+cK{)JADt+b3[/zM=t\'A}d&Pml !pk0j6-1+4M7:/2﫢=]FƟH u^g:E_ o'& _8͖I%L#F[JưzjЕ*}Fc _oyseu@uM$PP|QOqy`J<&v gUYqRc VPߙ$͠%]M^ ri(Va/Y:e=Im#$ Ǡbl|H l0jNMA%747ylI²姩fѝj (ƎESѳ wf>-tlM٘<Օ^f(Lj _4km̑x KI]S  }?uUm 1ɹ@VJ.7hː#ɱa:$[ídOS4L޿hR>yd $IQ)b^ _ 㨲Ѷz\} %X.9Cṭc,q Ԥ9* : qՔ-bjkvv_ۙ+I#J˶Urv 8#öF= Kاqq33ZFx}|$ѥ]H%DW)v}Ki=鋨H+rccmN l {yҽX;z%q=@d\&\=(v bl6S ^9`UgwJ(i# jݏm;?\EuޔUczcs{3ral.ñ#Mm85"{nuPBO~ItPJQY YL,D~s#{{r ,dpQVI}Q];^\>Cm+ˢӝ ŀHͤ dt+G&*ʅs OШr3lF}3:íYH0N1! ~rfsH('r:1]2:ޝ/Z (ϊUxێnגٽM<2f42h^TFu1Gԥ6_Oj©<ږ> j/Vm0y;'B߳d Jۓ6o;dȃw}Bѻi7~w/3%iۮ _"ըXؐ"$xtSԡ|'=fظ^S5'2Ag%~Yzj縿Np3EMk5@Ki_-̏gyyCc cm5]rPKcbMI33cQ]3~LL;dU.ozOXj6uKlnQ>CrZY"kE8G{84L}_A LHCU&瑭~ML:1YڤBtiAO!h1ޢV,B(ȃhye%ND0"j*[dl㮟 nya7`"g$J1A R=Ǚf=$pc Yci )5;P_?tq$"#Ah .6뼞TM ݷ#6tcZMLɩhTӆϜ 5߲4bCGÌ2݈Ԭa3ĬkjR}L'\nDn:}j 92 CiBomV sOV&:F FG{@>$nw!np )RrQj|!Э~:ȴnNSs:os:t*Q(] Vl%|(35;%߹D$hIzf ^z lM4,dPF:Ʀ [u40cMno<~Y OPUAO(ː'a~I pjAyZg ;31+HDZ8D&xW6 Yn0Ҟ.hn Kɜj{B?(ZV1#.P vL v8+pu*xu߬3IɬHې_zydߌx.),n3PSnf0tPQ>l@ri@Ǹ|$M"hɠU->o5y̰;䚖#7q=%E0 > s) 7$^wBܲ |5e~ʱb:uQq':  CU{f ͐h< k-l>}_M}NzwUmawv!+sH}xyPG; T)?i8Qh@It@(L9<{RÄ LcoJ!|fqV< lG`CVsNtY6(bi @mU@PJ 9h>KXEZu^,u|J4ZҐyj]P$E/CmPݓ5azonzf{\$F*[I {?b׊<ͼt:lǼsuU˖KyFTs^y7vJ!^A@Bno5*?X=9C͂\Ig?Y3彙3JZWYCmLUɋUW6b,"s7"+־ پns(ڕ~hޏV'h|m1.K ͗Dո̞ -ueIW*W.3j7GF=i7 i;I'ϭUNGE}b=:WF ˣ+mQVў\,7SAq`M6r懼rSb$2+݇-hs;880@ χڠ3[WʭO\#<8gkGL%[L#^RrA_~XGJ& ::Y2'7B[C9"r .(gܓJkૢv 5 3ʨyNM=3^ZdHHSQ@9C{QU {: j4rB3M9٣MWt՝Kd|;BVcxe%XYP@ Ȝ%s/8S|!*No΢}^n6j[[`n ü3Fa=#y7fYP-~"kbWhi/Cc>x=`Y`NM`>,G]MPk&=\G)$A}6JTevwI@ EF,Ҧ $p֑)b"R]oA\F `Zh>%Aq~.R(ܭ6/Mܿcj'9)qli`]6}%N&:b6bK*;7( S*/GA~Ay 38>fזN럤?䐋%MFZVIsmsB Lyqqo!^$EaNKnl$E ]\8%92vT-@C# (ʬR f8zV13b%$F$ 1iꢚ7l#fq UPF)e/F7,bE+ ɤiHL9]n{{E1״Y&mukшȨTZ Fj ұrwoHD?H7LbLМּ",L[Lʮ.UH%켰ao YS'vu<^yZStޢL..$!I1keK}q:>kYiԶ}d}" $awb IWvwsCR&0h);a(ųmGA"ʿ~kXEczܮ'e6ÎU6 p'Q4uܸ .:e'&]vt詀 o1d[f?n/BR,SKr6b(t? Gk#$Flv/ b_3)('VaP(Joՠ"H=вJ<5l֙-#c ^|e;EC #[3$2Nq6uoTԃ'8}cE |!<_`c#8I*lg)RC~& p40\ &8W[ (mnE]]D+zkoٰ\ ލ_yjM}Ѧ׀j/ `# hVݤ ޛ<.,XDc0h^EޛvY/:Iع+;^APPbC,0lǥƥgfVKJ~(}ڙI{3@Ya zcsaMx!$\փ#rrtuS[Ipv_ ~*<椃{tٰ^n0W]b{0 @i1?"a)|Q@J.~V9c>&A\;tum P)\,w([_Zh fLV?' ]~ jM c]fc ]*p~ժMQA)FK)&o ݣ dگQ.I[Tdm .a뷨қjݥNƱl'I>?lB҇(O˜匑vR4:v>LkTI~r/>> zH*h@Z5U5#ڊbRg1t3(7j7=D S[o^1 X,D͘ 'STp8:E-,ZṠ.BQ0Gf-fԕO^7E(Q~0qIXCjxʄ(-Ei1MuL%R/CX /Һ0if*!)ԞMs-.' Z0*(ZhenO1n@jv:y)hޘ8gd7 { MqHDC'|9Ӯ2PTP{U&ߋW\>  xQR`$re>؇?pڮ:$. M>$m YvmӆeHc`d4E26{ r? w:aiI[tG+a:s_fޥtfT>3r`F?ؐ9꽂?/e >Rus0b[%:\ouv$cxi/p"d,|/M(Ad ?)7zTFsKX,z>a[rCQwgzO'i-X2~S=jjϨGs(HQ țyQYF_Zɻw.YPVT8O~+if>zfw6e)E,ҭ.3-Nttn^VbkZg(X|qe }QvRTx]7b"SMu8" DMuLJU>flxXùŝcX' 6A781hpI:%ezh; FQ/jU,E tHpPp6#s sak8O Z -Ԅ-JMM uMꃘ|!᨝`CO1쉇lcܻ5mCL[eRuJ9z9*KYMXOzf0n(ͺ0 y 2}oH9cspr$H7(f1ϻn j7ǵmKOW=:Sl;Im#t޻?BS i+i-Q>_|c *f]2֫+va;p7ޘ:``-u4h*'ytu|ʆ٨IwB" lgyf NCP%\~ZBCt1vO K5"qxC r8GW2^ӱ̪h/ҝf%ϐ%G:@4A_C*wQl۹\v(yG+[̴sʝU5! .9q^S&oz;Zf!3 wU0hֿ!)Cפe.moQ!t?ζAO+1Qp؛OBQ{?'[#Y[H|Jݞ7#NMta*WMaF$a呁z do3cX9ӽHw.tzLmVjb$"nc>#:6f 44Ň@D`|ζɖ5wQksy0{ FzΪÙI~`=téҕż9xޥ~Y1vIDh>iSf׺IpfXσ˴BWRAeއɕXrY \U8> 2iyH=46ZD.+W?Xb[SL9ÒL !*Wϒ '#w"K`eq>sc'7jDNI<-_Aj?1"U4Iӻ:fĢdyF(1r"Hꥰ{>&hYmFaNXyptk~ 'hO5^GHIuc~!>uƛx$XȠ(B4wax6m isqPOBfYW!GeUԛA`u RC*`MU[@bs ,/F 0rEʟud+. :C╒km6%#rv% e3UF 9U$`u%WUCquNN<_gn5cܲEMzǫajgDˊ?Xfy,cg9lMҽs`Ns(v*0`,T>#hک#_$vc*zH6,UjO:Ӽ5Ge } TީH칡$* |F{\IY!jM%AXs!?eHa?ԈE|?׊xBBr[ƚJ1\8<5ytPfK]8X0 T`PjS dذyŀ'I;۹MZw/Y 1IǐZz7xNxcj3g3Ѫ}0 ْv꺝4}n3KBc4,d=٧)W݇ ~![B (ͣ?L9W6)@(' JΓ-~} HaD1RO&IoG0e)%\jVoQe3pKǓ蒦]*\} O0' BwѪޚ+^p+c4bܸP1"}؋,Bx-c"_lKI7pO^H{81Y88Kf 8/>S&gG5RWK ә!%,A$em0ەRtAZ>O4wSå_ƜF?#`%"|\;l|/yx>? ‹4IQReR'd.~)$$ 8c_ &Y%ȟ雹f5-laVCƩъo>-i]}lrk_c0>G,{=9vhuqb}sT߷skmͻ^Z4ŴYj.+yŽņ|Lܸck}L1A%uޕ*! J*jGsBi{[G8n/]*!p2BF;Wȅ*TqXt] PvݳMυNRDxb#ý:^=X!d1܅e#BI.%gךBیSt[4rPӲfPF q2"z;v b VCn|N|g1pYܼƽ37=_luv䗋)R #ZiX %vJ ÛІwGGArβ-v-u!QaV⏯KS-љz@9'+&t53szS~JPy-z6V?2yldjcAUCc;pݼu;b6"-VrL\JVS"a}k/{rs}1dnF{k*>p Fsh#;;ܑ3bUjdcE%.Sͪ=+p1TV1=lj="| Uue/ъpz]ʵSn;R˾Bkf/7R?jgoOG/@d'RDݛ!DEC1(!U=`AUFRˡ%gJ̼lf0+K ol;:p<_D$ySڊZysKWx?Цdc^sq@_۹2V[tmbˉzڕ.`{V!rލp[)@ZTڒA@Q^]t)mgG  S ;6~M80nCs-_.CsdvhI#-){\;k IL*fuLRY8QK;3+ -jEzwx vãGZ֥5jW&%A$qFn e>h`4ěC#pgN6񚓣anY9i.M x/'+3Alve* 1Vf@&)$*.OXiu_ _^*Q<7鄉)=;x(ԠؚrݗhKD*!Abjz<ޑ ͂A^II~uX[lNW^T.8U !T:riW|51\J;yX0m7wCwĽH_\VsaZ?qwDAA.hA@`mP>C1p1!S fZrWRϊɄ#w䑥`}zs]6gM%ԛGEIjSɏ#@\A!)FfRōƻPLG(DL^dD/Ę2M-*Fg .<)b$-c.'v'wٖsyT)h"uf:7 '`xpjt"&F#xUZF_ab=ACzΆGE ow*ڵV-}^8@'xUI&(F&ǘxB^};zF C+ GzWLMg[jNmVbZR)[}P8SyJ%W|$uԌI|P-לAw vw-wJrL<0f#)$jO÷D@[t Yi+q .~"j>c} J&!1R{8A )N<琘u]k `)z<֏.`vhxE xSl7TEogh 085 fB,r&"/d O 4*8%'"MҁAbDl0uKI>Īᑹ%Q[,>BQs~JrlVfh|!-zwKϢ*Ò4߳&,rjx%[l!`zκQ`61$KOc&l$){,^pJ S_{4d[Wh o&D$6/xFtZ[X*kS5!V*Y,M/"~gSP6ՙ Hh=R 6[Uy_STv;?-i& m %1: Ȯ{%-mڽW.oBPդF g{ɰN'hëmPd3?SX^TI H[ Cؗ*ۏG8;r;!lk{ xKxMHPo~(欨jxCԎd@\'˽iz-^LCfRv`,1YxM1-wY6gTo[iw6G޻FCpj6hakwpZDCAbLlV=_qek+I嗆~w o AG[Iծ)M3{ЂޫQ&k_zcO,b1ѽ60֍%RTTY LjvA yͰ,s6hǗV [9%EPVm~%gu"U::%Cˆ[[q)XlN#]O2d`{-[!2c#(mCn jU +!slܹ{5/6yG@NuӌHzt$у:`mIQ@Ҫ`U7x5!a'] 1T Q6E}6c\-ea 8G !Ӹ t͙/}/(dhSgFӺc2x{b#ꆒsQW@ ;8nBGliOE~\N1+wk(݋tv27^u7@ CLD5~o)P1u[ 6*-;gLXvE#CScy r!dkϛJّ]֝9NStOr2iaG{!KYthT?Xls8{z)pA7xM. ^ BK2q?#<ಠ:5U.E yO\o͒*u5Z'6ihXVܴj* ){1#$ kRS3xG4zFL IxQX>gnZԹ!su4ޟO5Ń 0I sá t) 1m`ȑu1H: n#o-(y TN`q+i]-经nzBHb?- 6!c} fYҌswik* hJ8~5ag4[#)MFc(ϐoA&w62}cYqxƼno}hZk52 YXh7+(N0u_6-K[F *;*/VzuO6!=QϵtS:{YqoNJa:;J:CEw`ÛfXP%P-wGҷm݄N9EU-ؔX1;Dh]Ն1P^A1պDpzw, %^|Ο(ocd?4~9ޕ WZU ᥤϪ>S:g(~{Uп:6Qz Yʩly*+p-0/pS|VHɢgxJhCAlZ jE4cy&/O ePa^ P)e+6ZOt:QǢs)O?"-$/}.kėGLWg@k& @"{;xޖPlե!r");+(eV( q[r, \ե5IN}m$2F >Siဍπe@f.dty@(ڎe/&,BmhDƬ4z~ Aߴp#g2ہ&Iڃ{xTGDZ/oĢG3e-aJ qYN*舁JH|/&2wN f3`OlM!FRA hSFm|[])X]J8WgDT{`_m'KIin-5 /oetdu|`O^6hV|A=Hc#wS~iK:S9E{|L|hԲ<Us)CF->^x .vL56BгeW3Ԇ#lo-L4bnc}0"V V,7/]S˟_7|3p4d2rL'(H7-rDW:D!LHVV,B',Y.M?xhkm\0n d}t. i3:Ƙ;yuŁ\ %$? 9ސ{S0.x#Gӽk†żhk#'c{8C,p?"FwtHbHG\^q4DŽ4_\. *gQHP+CJV0BP5;6*`D.fp ~\*,(:rIs9堪5t"+rLn6@uT7 IW< ˡL!CrmQ71>:fڤ{D~Q'g ;왩lR$ g4d i:M +d`cٷU^wH y=i;b6{p`0Kꐿ÷o)IܜX bF=Kzύu6CMqc`<]Ɩ$ W;R 1!qF'{;Qbx൳yCjhPRh'*Zǒd2J9?;H|m2I"Y[[i H6F ѐ#G]pjxhuY+A9 [HwËU6axHֺaYd=ȔZ @_sJIkNV) JV†p)%~`S8^f7YaXL%WoeyElw0.+ʑCFcQJ wn1>-3a${AM<1T0)Pu_+DnHC:=`BpJ#m fiYN$ilb8A yQ}J%Nf@ng?~kR 7_#ŖbRA>gG.jrR z-} dۥxϺAv!S)Tr>y!&&|Tyau7Rv7v%%Ȧ=^k}SZ&)LCKaxw!{(F͘LF7j앭=S;wôud$́eQ^r 8TwWĢLd4[]K, /EZ+^3_6DZ-30o4A9 k_n>ϿRViɊū&Kۓ`ڂȻr>G t 9nM)zcf]^``~n'}\E?V'<8јtִS!mq={9lC,u',_qHrF`Zi9Өe ˉ+ͷy7v" :!RT#|9;|t2ՙS H]HJ3=/&Z)髮aϵ@}`/'tg_dѹ}J ȢN6PqȔoK6l֭ AO%Yn.l𗚭Z!HKmi "|4l[rGlxmv{_,: 7_4ԟ6bZr`VMVWI ,;)~XG3t]cs:RZjS0ʑyA֗zާ:ݵg=$W_j^A-A :I $ 7dtyx|%Ki%El#{9g24%[\!"3gy9̀+H4_R3BQ y/|ݷ0yavFXt,.Cd&37ϐm3sn٘GI KI%@%-kP::=x#϶S@l6 u%3/E,DrM3+yְgI,VS%2j@;_k'ط\.`K!U:>qe+5^`NH9ѓCbQV\"<2k]DXEYFEM0肓 ,s!-uR]R+2/\ăi+(s7FZeyֽ[ֲa&ݘv8J^3./:^:/0YVBOOu2e2׈_ʫax$~ﱴheGILE&>ָ#3ahkL5KfyE1ymDDʽp\NxtHuIjhK1 L% lq"L=E;;v?֒e-4DRe)@h$yj, zBwRcJj|M7$=b<#p9흸 &ǩȗ1W:_5f8X^KgSoȥFR6 |rf3|-]F:O4!?5pkpLײR#IF/Ho әL_-ڐŲ3{X= ٬2 얿JO0HSp&DJg%ϖ ːK4.7t i4wg[j[˱h9|:Tc9Sgw(FN$t0yn[%(izMx́ RP8\Ե[/\,I3Qd$.-[cki}4Z *34EToI *;0 {KصPЍTn ȟ_H]r(Cǫk |Ňhǭ|הǴj )ݢ?X֬G³z{]tJrϖ$ܙHXr\!'{E/=)S`,Lg:۰aׇ+~=0~u[1a4tZVfD-[|)6&H6f0y@AfWX3F sAHX? {`H!C  K {7u0H~$҅[ts33$l sAٗq2R 'ά GT*<ܮM*H:>@ dj3&ױR[P]MΧ J3u<}SHm♖@%'saYQZh -\vrs)Yv aR8&I%es d(Mag$Qf[vj/QD&pRKz)\B3Nةp+S?E>cbVMswI?ZC_b")Sq8ewy* {A@wnz 9&U6QR@󧝂!| ?@?g$P@$b3ָ_HJ8N'^D<)_mu&u{Mi1@oYwD{Y`e(LPi)Kq3zxulᬟ:ѕ{\^bG˴9t^'B`江`ofbٗ86Vաȹv&uf&Tq}nj#[[^F5XQu|H$3~m9׼]C P2dzMg|mEX&EN0k^ew3I l uj\y$i#a5Rw3:?C"Ea`|v=7H<\{z('ǰq٠7ov\V'9SC7m_9a[qХ_5@jGT\Vl;V8!:|nI2/ӫfH60Ҙ2;Gfhk(i< InzV35.+Ԛb+갂@P*5Hpn,};U^Yh~23 {FWj_Jѝ ^뭶)h,v'vinsˏkw~1c0 keWhDcyY;v\B_0oe1%vW #@fnRDN)/ w'vS{„(ZP.C-S1t5@[ V^fb AoOV7b J oMU"K &d,ȗ]J?Ưy I\A'̣ONl1(IKfwI}v "cF0j5kp2J3fLzb̋XxIH73 0LPs wxh>kBWҜ+A?6&P< lff oL:ϵ|3v5 {K.j(iBx`i[آ}LڞDoHʷL@ȏr*9Lo`T"˃`?N6\l:Ec/GNFOO]Y_&/YPxe3hD=P~3 Ƥa$OVd l:V?;nPkf$nix[ڕNW|.PoAP:'MX+H!ҏO9aTv政x&J3jwn[ DzR?]J#it9kIy wr; IBrC~9/Wh15A˄{ _5E'>yO%օ]@$[Cm>Wʅ5ޥ$ir}ATa#j9-0ﬨes|eig(KI/۪J sǁס63~6R㝬Q!Gp\"}:̏rxuk%={;Ϊk-kɵ\d8ҍ3 r~<[C U&oYU~*ALL7zv}b)[g=7LgHrfC2{5R7?T1pIqL ?qۏ"?!~d €,a8Rm#4"]1T:Lw`fނICp?y$ I H-ʟ+h*Yk! *YyEa 1>~w3OԔA !#^]Qb\|J$%ŊᚙdloތcTZӫ<mr!o{˚]PR/?xd'dF$ ̐`ޞX': ܆&A+clu]Jr/acLy ~Hu;f=0Ǟ%-,t$ J _d5kDaWJ }eix/ ~iH0*IoR8 ga)܈pX9Rˊvnrl7f4ڇ_"+o^kKTMФ-zX(`i#hMMzwةsߋU0xݲW;%^Btkc!qc6n1쩃@BKŻC>%Ɂ _)fF\ ^[)\|V ]Yy{"Dx[ 9 FnzXࡇ睳 |okLCӸ{ }~=]={f7'Q֗M恵x}Q8P`Y.ͣ7I"ƅ=y+(\KFEgC:4TVzϏ+˗+r5M|MNw`qp w%Gn}<&6qCA38ħ%kنd[twi?ةq]7~Zɞ JeRugC#>ɭII}&mT(/ .\_f! Cr[!_W,H$W.6퉠nBۡ;zۇIKZ z m_RU<ߍ=l7HFp $71R4X9uH*Q[qw/_ѩl7NbɱY8T󱆘59ֳ[7[Qn,i,1G Ӫ(i%]Y@er>H E$⇊]}vQ^oA>LM%,) B-Xrx^#6%b^LsSAfAQ/h6Uzo`$2Q)_op"ezkQǎ<6w]ej o,0+T) ̷4΍.>%0TpzR=bz.ikԄ.WiN'p\D%ԖR,2Sr."ӀpAݏJ,R#B_^ 5-KаԪN*m.Y(uy/i.5g•6aR)Dn,FGMBMIFD}V[[j7=‘L-"(*I(VgA\8;tjw U8%s輕4zAj+G$p7|bAʭ+wheNU} ";#-xơm#bRF.`heD Ahs"s-ϸcJ p"9SHaQEUD*F` 'vAv֓E,RkAZhLGcw5g1]5%BEHغK"++Ҥ1w`>FF}2?΅ W[=WAt1Qt2/ l9W^lygĕ9b1 hFKVmAo2;%dɩN5#J'33[f$O:wǍwYhPreݶvIt0e 6& .FN`*zMğ-'lDE˽!Ed+|v_D&Q4*h08Gi炌Uv2[Yx'#G0·ҸL0pUr\}:9dEZ]W#:F?K:GsJ@=iČz#G$M*A׊o8=UXVx"6Nr.Èyl>fO%:vXD.,ޢ+TNZ4< FԪNi#*v- g nQ;26ίM!·g}˶CmAi#rBWROQ$Ib*+*!я> qZÆnE˕ ؾ@O炁 iIp4>#hRTk:ݏQ rNVCfo`M䍧KyQlx[ g8lO4Z ,6f6˟1FHj&4`ΆvLna$._<_o*lKڕ.]ƒc,Qk- Vޯіu({iX>@̰ۉ }y/ їaY6}߉nxJҭ٫&0J9M); >KRϺܢG$><0rU?vLP1'[XE2W= 8Xtu.CץM@`;}ฒX_7G8*e 2}-Rk}`T?(=}2`3.K7qJ5>MWft ooFLmtzY5IT ;.ǽ`y}Nɠ=ѝK-CLy K[ O)Q\+t9<QwCy6M64˳K`"g]s[ҐWz[mH(In $Uï Z|ϲT7ٌ/dGmFFm8n@7*V*kϪwWh~*#OhGUdgF0U bo,j tsAC˴b8*y .vpGk04M}.\cd2'ޠhnuGNdJw:Q`r$TJ7jy#=ekV6$fG{9k>pEl,sJ .d oltd|o:aH za Pru3hv|n (3Fғ%ܨ߹Ţ{5{ܧgC eZh|6PM8]^+7/AQ&MCoȢ9 |/Z}xr&Rykq\S+'K&beX5*'#h`lqp:vJH7DZf0Zb;XwMMa|6z.]d3̬֑k?;lG_A~qc^(;G*4U@Y@fC{E$2$!tT #Fσ жAjźfN]L]J8)0~Vf&ǑH}tB4!P LW8Mˑh0]?/k^fC9i$6Sϧg5D\SEt,yeG~2*ڊ;WseG2nl `vroUMHB^rfL88!y'pcd,/Nr { =[z)A$Te*9+3JkPfB([,;eA(AU-NZ;o@?wI<˂Ca|QN\c@g Eq AW,sJnp1R ̕$/!AGbU+PB2"8 ~ lX0@.p (.fjGwar慤C`cb~#aK&Z iAٵ`&Me'B/ǰђ0lh]W^07*BL܁aZ~ĹS3_+@ YgˢEALyJh;=q'6gLi_VUIJh֢*m?u6z4R|;lԶ Lbgu /xz& >gc-LF(/>^kzwm5!_thaK,ŝm<:|82P"PAo@x-cwھ= hea[|sg$FQd:8{u!܁o=/Dӗqq#'NJj!^&3 )7.юdxZ}w >pe#v̸m~' 8 +3ۄOiCك~NB:0䷓VC*_>>hNFs*P9]pzˆkh3j K{pHv$Z o+# ep\ `F5'wӃ& Q9{{mɆEG4[lz~,žn PErc/#+##He<@EǒJ$+_@:C֖_Vg9Ua= ;'I٘\xj5OIGjvh\[Y ^&1fOpcOcXYEhmlAw"/]9z[%#DdsB,l? zl#2L?!Օ ƴW T=yD JLw$i|J[LHn$% ڃh( 'j1PpvDf8Ww2P HlUKm0*ȋFx%C#TAjT2sIδb:1S}?Q]@d˦|=E2]O8Eoli+0s<) VϭkKMB !yz#X SOmZ 8qoبSh]vD<99|71.F_`rS"g]V$< jF QէE0}䯥5)x:e<+YO:q1{Äѡ-$"NXt<zeʼngӊQq|w0ԛ]0mE,‡*=xG|M ^== j'(͕qlS~|쭄~qvw^&SH'ͤksnP~?ۿAw3H_'x$(M2֪1$8pq(mpɬ\Q;Ɔ};'ܢNt_[x.jj>q 9U*YǂC7},hե䨃|iGöoEɦ(T u~qt`F&Ȣ\wUU/p=7L|4h}Lr`M>d;0GF^l+\ZK7l0[pK^tMA2'&vI Ű鴼8/dX#dT2 EgM/z,XN nb.: K/?6>bހLGEӑ*͢vщ[)/f ܆ !TFaP~?sƝc)%,V_d׬"6|l R21]P5<uSi#UuPvjt~(37v}4[39|s^奍c T#Mfj3Ŕg4G:I-[Om!G$ޏ2m˺pڍ%.eۗ/*ZR T:kBfGc•xo+ۆW;-Ǫ>ҭ:r o-C3r)1vAYA綦>t{^G&CkX#N7)W15"Mu@{ts^S9ulIn2yo{[PqEw}am r۬ՃDS2>xQ~U=9LzhxcƃA}ØEkokIe &xg%7ʟA/"w[-Kgw?Hh! Y]y?Dx,Wfj< saaK#ǏXFU.m\9P3U֜:D;rV^gۍEjs۷Oփ~(G2`0$2OM7VBĿ=|I&91ayK ҖYp>gGՕM$j `8c]lnmfFk$S-h"_^sM\yYQ5*'?[fRo~m/}v_z$-jn:RP3s]iZ̘T,&l|hefT46B!TR5eD> +]2*Lln\'n],sGEƦ6c[M5<Jl[&-Ȋuk1ko,[zX?. ?ƐZP 'b;ZرqNj}#0@ @E୛(S*Ѭ;l۝_,kT~KKBzcFe!Kc$lXA4Q>^ɢ6$Y;@*? er)75Co6}/yliqJ)R-ġZbv;ay#e\uVPobF;lyRypF;6sEczZqG n/]nt2Ϟ,(pnU./45*rBڸMӰvBAQ au[q M )y޲(?nwgCQ@>@DTb <0B"L kQiuX&X~ zVq*`Rg01>XЋV4ۜ܅O;fɿQ>}К7|Daf&`n=j޴ԾG:VFuN;ͤbL&| fzQx%4JH=;K+88(s-1R74v=~cn?f Cj R p8eCP:[ABͩTJ%$ma 5se" $3AW dln.ȹo=_'¢BT-_舩;R^[T=G iuaړ8xqq"r7TSvj۞hTKާUj:y? 8f: FӔjDid%<}u+jB[8Gt],f^/Sc{H]اw\Z6@*=p.Hvb?nR&4K螷u~q=C}dtz[ 3dVK;(qXӐ Z8SlO➢,bU~Vnj$Ͱi; 'BB[7/:FǮijW?[G' qËllD9XY 눦"Sޟv%BޝpRbPEH<384./3h22lZ`/-޸ll_uLfhlu28I/>gS!DW^Yu֞VyYg;s{5q `+nb*[Aoy/Hz&0$9A)1Mب3րQY=VC]2pokQo_Xi{HyDMT(km=6It.UIfȑnzeB53M rÙc+.<;ǡ+%uVܑl(N86B/7:qʂѥCkVdQAJQMr_3`pJ[b6 OAOܛi N"AR* 7y`wC#B6"hl$Ԇ:TcyVYmY*ńk%7 )P.0e>84ZI\''o6LVa]"WTRqI"KKW{l>z we$6n$1(LTykWȁae\8U`3}>zwY*C]%1eJnuEghᑢROxV1|URHR)[0La^d8sC?aAS9,vS݁+]d wLr Ď T@+Ѧ'Jb,OЌ7eL\Jo#PqbRG?) kv^ X_͗v: N%Lm4.%@\X֓Jk !=|Jy%UѬK4 TZ;R[ -hw9<BཧwGтAK[oG76,s?zt2yBucybX}[n+eh{h lfX<-t`3&灆htB{#{?RZܙ{"n|ᬽ;Y)V LC҉凷 HMIWrL)d\0r]7ئmk~ oQu(y0my+PKX! ![ZivqJI>A!bI[ǭLɫs]LJCWzMuzjϪȯBtxT(ePJP*qLӘ3=pxr$mΝ *ES@6$*>et%Ƶmn96F`n4v t hFָyS|AqIvV$A X*VE|Ŧ+N:lKbh:6pfU WaZ7.b$'%W`$\&Џ`pقT7 ͖uwQM/>nlg 3B RjxEzQ® 'cmǣOtz g9вNz3#DU䭆=]S^c e{$ 3 N qV*\ݚ7 Is0.nȺIgMrmKY rO!S]4hߕ;^@Ӡlsuܗ~QUY|~Z! tBJ`7WH-q0B|顢\5:6ױ%9jYʦcV>S."22q68"RK;- àP\:bCPf j7\y:9 $؋&E\!<H Tjr)0k;=I~/c 0B4|uRcFCmͥ"3Fp' R !A+Lsj6u7:PkWȈڵn#Iз[kïm͜>DRl6!wg~#@l|7DGKi۾z9鴢Ɂebxc .=vyҪz*)iOI?ql|b֪pGgF$ؠ >r8wLZF|:_>aPIymfA>!ꋜ#0!ĺ .A( 8": >i[YOY; $`aUJcB͙+?,ni G = vM- A: ?)789#f?q`1N>$Z[+FJ/Hq/5 9J`ZjZ/ȶeZ_$z¦- I~db$'^hS!`b%Pjqj$` Hl塈|Q/j7FZ!O\0GDiVz׵P|Snq/V,m?$xDSUN^Ppv7hLx5Gsd~rev\IU;ۜ:FA45-zS9ޒ5gDHDcp{ \{~(=sEK*ZY$\4TqxzXNuDX)`?9$,C=$AH}1l֠``a88 _Eoz|-(I^\tVC;sԭ4s aT'5%ݩMmNƠy'gc|C>:W>=mjBL^MrpݵSNܞ(L<\ƌ=:xVd:zQFEcQ{\Y1ܲ6lءtsͽ bp(SA!w­[ 9{%ju uf\:7)0f+`ڡ5Fam]ѝ_x2;0ϻ; ̪W*ߜ1 xl>_Ju *Z6OC]& -A]m뀁~m( w]}5 A)l Ml4\ Jޝ&_hLlɮz!c*6N>XΉPwͤb٣h~Ukl~J8$Q2g83|D){\\Rt)$101.GvV&.ev5[<%%TSO%Ы3UmQ<(+OR@)h]Nfmg_9]k;V_h 5 qms%ԉUi'F*mAK3/\`(ӆ1{ $/v;I$ Z2!$YJ+DBKc [HSI:H`YTW'_ qW#~wwϯ$-PwZaS~j?UbaY!-GG&zMƶ0Re$ ujEIyX3LzΗΌu.8}qME̹D1Vh :u-d^"m⏍o5DnPkµl&64mbr#iSpdaJ٥83k#}rsXʠ.b ^Cg֛g<2͸^ުwPtrN,T'SlrI"j_k]Ff/:5yN\jÕ,*hcGD2]r_Br"h?{ 7"s9C:8˜c`(=QX<'Hqo va'Vr4ÏTt˿ 5Qsd)ʑp4[q.\"`ʞL|԰xSdw:\t٢RGPqimn_vGVQXR8t\CUn6e<//vj3'+iFVShR@gIo I/?"[r Dak;}=\N$Hj>cuN@-;/O9u6x>%1y ,NA'HT"{У^v z̧ז3^,]JӼ֕ =by'ŕ^E!UrftWcR74?&rHETTDY<XLC5]vPsEˈfmE+h"] _t比K&D@& Rns]/2.^7܌!$|(%R-U"xHG[Pʄ^YUPש2+ P ح deۂB4S<*4MX[+YhmEUz;pf-W7.R~3TeQ,f6ݯZFfM-'GAVdyOA;nt,;]4%4`كTh+`|4+-y IQz Wl9;9qDRGsÁ5 Hx%S?xԧq%k+76YI*[|˦XV>K\,x=~ ? + ,JA.VNx72kr]Q j.EIefVk@Ct9u Ԣ͞an&d;:20<&|N_X_YFlNɖp${ݐ/B04TV2)$ڼKj~G硉y 7RܚYN'w/lۆLL1\5SW.ō58/.&YF?"J(iIrҩ~SOImL K`p3t_0|JE]_h kK)|Px Cx&p0K&46nAWqHǶ`LԄ@k݇ HG4;!tJӮi`rS0VqGcA=>^}OW/=^FA7( ]_lBoxZC‹*[Ө0P(ʷNtB"Nׄ,#J\p;KSa{12k@yY s 4'm| #[C};z4G/y}G 7Cۓt /,hgY3Wqsr6FхNl\몃#ѳa ;z%dh/0V3D0_vEB#ws +q08vDt^4-ɞ*Pi :]mK`N,Яgy:LJWG;iv[/~x7ϐJ2ԉ?Ӽ=!|ML`P¨;QP/<AEً4lݔi.*n!wײ%B=k(j%-~9 (< vkG Ƞ᩠dѥ6y@leR1$XWCXOfp΃1{1 gf<![-#ђZ;Qqoj2k &lТS!m&K[hyU7`ɽ/V\Grю%f] TE*f )0F Y#s![Uw%K^pm[]ȝrh$nU7n>`^uYBBKZwdyl3<ҹ:?PƏ?KoA(ZިI'0bY %]j&o'UR?2 ~F+٢ k)>ȡσbz&hlոzK/^ˏbiIBjF~G˳32B:n B\F%?t2:h~=Cݺ ng$xpƧ2g%VJjJϖ |z-J3{@])$lqtYO kvS)$$y`1_J_h`xTQy.ό`w~'<)1 Z2|yEc O¯LTuUu45] TRI Ɍ5&s*Βf& g`{NxH' ubbr_9 <m,R`{.Bozpi:_Ma[-C6^ DjqEkT;KVaYUzkb1lםW\ƅ* Cr (':,vxCEp㊸j Iaﬠc'fLަxH"JvwEE]ԠJa67q@DEyc̛zzT.A9"oSj.?+omߎ䀅ޏN{XSF9n3>%g?Y#xKԕ+A;6vËTei%9Mj5Fvl4Zf/YgCp -7ZR^5ՏOnvMlowڦn{k G[=ٷWƇY&0I9E|\q" kDݜf?we\;W ݭBOsyX(f~bANN \no'sHRA?Uul ?Tudl@UMC5jL-F/h ؚ.) &V39˒MMW8x4yppGMf&~O9 %kuĔ,{s?K#p.W[2Aߨ%:DL"rhy:vduzi[G@R  ?'#RflSMg>m c E\FoHWF;Xkmԏ)AϽmQlCy B,u7v5?xT/ CŏF(@?9׭#\Zec~G\,b簾0V(yBsS<ف 1M,R|V{.Y6ؒa2%xy?<4 1Rw?/`0D~i!ٱ7 }F.jϪ s.]QMf4 ;4γ?Ss@ؖyt`ӖgmmH\[n5DG5k6d~#°Wq]{:geSa&50σ0p\ 6+ &\Q,I/I ٶ4r !#*C?Nr,m9(,JvPk )M‰uvW[QcUHN"D/o-g>XU\RHhB3zK` t "3@@H!/FuyݓB#x=?Gz@u\s,1=.k/jKO2%&23CtJz^j+!1(ɍs'7]Ij  =,M`~}V1DF_[G*M8Ir7V'#>LPx[uJ79Mp٬|͝|3cGu{?/j_ls" H[,ɉu [{9y&E<"JypB xY~O$+Jd=RF'JK}t*q=wANQ6qmifq:]`-.jY띑L4ܨX_/ɭnj٩GW?ۚzi N$A(  2d(ɪrByV#\PZjؒK4x~WK=H\VoP؈ϰ6g}D?MN'ى1-ZQFLŨ;eX 9iM2aƷ.ak= -"ƯnI*R/i{a!@lI:iiYQpxqiO sbӇ! n߱J+gW+0֘uGzQ˫8 =Mw:{jGncQWBDT膄J(t$#tĎ)$.>%*3aFԺ^sq[ E_KX'- lCC`jH!v!Q·rsJ-H$RyV},67;Wq4Wr/o菟 u\ g|sTNڍe zzxjSY,&QlЋ gI4-lǂ/(NBUw93FAUTB6q zh jVmy"Ciuo&84 h=c}sׇ6jRz̐}:L4XwV؁H7}bުܙWf`0c)Y(N".G~&U1I-xhm!$oEl цbLO tvqēc*rR z<!*P-Вs*)׏w8CMAKIWO7S-|G pa!A lnr/lY hH8 SC>ɷШیNatE8VT)Sf>>ȪZDo;d.NGpJ'E9jNYޑPW^oit]n}:Qɮ3u s zZESzyb$R|bx)M`8c-3Mq@z:0@q$UEC|o+BlBm{*x$yog4Xm6xJam}qz^:~3|Uiʄfdz4RD2TyL#+JCX+a|t'yNQP`lv4[Sg.5>rϑz.;uC݁#Al7d εE$Y41R*o]DGmY <):R;ȖI_CAEd~qEkmN =&X( 2HC$cJ@c QKdFOXZ/Iɜ=+#,%gg:B.Hȸ_υufZx',,j|=dS!* @m#izq jtk˲2CM/_Ğ%+ܔM"vt;ݲoR}[8^Y ڣ}+-ġ^}wdXj.H`8&`G%._$2AQ٘ Hņ0&sM,‘M>G28A`m5#j%K=D*)93 n,XRAry3j>/]~>fx猎 Ox(a8e7 .02_ް1g·<$`?ULQa탹aEy]VuQ:'S#g.NH27IN  &9{$-0x:vSJZK佼7M]E=t"]cwyVňU1RETl˙JH1=2޸äwq+ʳ 'W)1@s!s2-Zd [RA@?E @m!NtsUF ⴌyE wX~YK.۫[1u5>%ʖhۮ64SFvLD8-[PY={ 4US>ewHfs)n8JߑHˈR _7BpȧB"":祼qT& g4j1iDQa H4b+:\$gƲlb~|zEwWqEw:S*󱊬_D%!t:?p?hl;Z i>-Sc(r V͓VxB۩N CFPEw`5|mp`lQ(P5xҭZrU[K9f4iebGD*VB*7 ׯ.ۈ_;k[b>)Y?^PӅ ;"ѪBj XcxUMXF8^\|;ضe s0]p#YS̨k{ sxm._xumU2i qΓ]F"N(j,"IyIW:#6$7OC=QB^`1;swr2*iY߱+!`w> I(m53wa>ʓ=NxSc-{h@.Re`AM@GLSGOf {\,mW1tl!QU@; 6P QG@xZUHiN9'eג S.fmTjh(C.|Rࡥp*^hg,Y{ڔK}ޣfC9e9,~ _<ß$芊waۛ+";3gUj~o>P8w9!+j{ /7L6RB{̓!2Wa]I[`g(@W:PK2S[0--nۂfɋvgJyyAFJ]XbM<9#'a骙bɖ4ŔYu 1㓧)HQЧ~ ^ގ ۊrgO߲GDJZ.i"+ʙ΀림T)h )_K+I7|َ w1S:|.r݆-Y1.ZieYxȟ/fbB*}sKNL c~,8]N>[taKޣʲMO=љ-W:)湔mkOn㧠aiӒ҂XKUm@hqcfԦk4иNޏm&T5P (m.T[di^2ѓ 8("|LnTM+u}a* z]8pG9% ‡iqq =/0[:5* է)GGX3fs{Uק>0ᴲc,/%O,. mYxS% LV 9% ݸxC(c]͞ʸ&e5I{&F;TjkC:-h&EoLf>+ZƺVHm-rb]Ui}b34"m(e&|{}ӈ'H:yİ&5Y\h"C5򜾻_˦SvF>b uV M#'.AN$"ԁ-9-99Q)SYXI8(|%OLP@MyO<۫a _ਠO̒>?W,u:C/nZGhV#x.|PԎ(T}3?k?UQh#` -O} ݿѦ;蓦 ; a@ 8Ci>'Y$_1pn Ar iEP`FƜWvgF#aWW6w5ppȽF?TKFȀ~kzJVCU*1!zk2I#8eWY (Nm8(:m_?!,Cf9fP_am qXpr|LjTU@E˶3y\%Qѳ/G¡VR9"@?T>A~,\)cdn| aJJ#[iZg\JU19&br!c5yY%;FYNK8: SӺ>ᑰ9^\yjr:FG $JŃUykaR=1()33=7Ymm:ϛ5XǴc˔SzAq`^Iܤ=Z%+/mwxgmI|QHؙè#Lw3J mCchV[0`zp ֹJS_0d4D^lճ/f.3fz"?M84Wo⬨DN9i7ԋZjGriD>0yUѫo+1(zSҴB<GF =~߽ӗfv O|"x{xWֆ4Hyk*EÔJIy~ j*F2ah,JRS{½K<.SȌ^-E:Chg9rt-+[[.b^0  {lhL|Š B6Ѵ2>醰秤] my+o^/)2>2/MY%y0FAk׏ջ[Ex{ci)N_B>#Xa AQ.g5VL 7N@! zքc[uq,:ޣg(n'\zĬ,9Sw&+dbrpn{LrS|hw'՞cUNfQ Eޑ].B{Ogs9"CH!kWhk޼j#>ۇ=D\m'V΁O)0xӆw)_9Gqrf*%+l9mT–yZXFo"Qkj?^+΄0YҦbrx&ѱWAcWTkTu Y@Mc ػWO{}:48W]=5}a$d>|=nw5s0m`M'X S>T^`ZqU@m+w]. Ni'N3_]IOoy^tIYP?͎:OBxbJ]tV M;,SmdW;\HzVP[lma_]m@mYYƒ}ZTtuT%ܢs~+R.2EQӏЬ$6|! ?Խ%U2p"H nfJbGWz%֫j/'8. lKh̞'mf'=vI(z *K8㭈KH/h5+-d@wg"᫿~J2K6oL " |@S3|p7W&;"("ph|UYfgăyedP`dQeӨ24|:*((QB{4ȗ W*R01,J+¡*d]&]ӖRꑕ/C䦑_L&3?&f#KPZ+#;U´(@/;i LCBf"? qW$<}DZ_^d[7HS^;+-I~g6SwC <[-Kr1C=%OZm>~F67/m/ a/ִ%$hYɊ |_!&S|ɊKH{2 C+Ek)rLYYZzȳLǗ,DfL(6[Eaf%T쌉W4 Yϓ«ZV3C~n Vm8|P@1N_ Ю\C-մ,r'm)FA )DdF@q Qf^!x:p%I% z6T]BE^D)D=&r[:Erk *Q~(z&Q9Ɨ&u@BWa|7i,?Z~z@7ňV<>0|˪|OXQAL6I4v=0A9Z*]GA<.M^5hnuRmS "}Bx7dq9S>0ᏔSaAAl{+OiOuphf.„e$ѤT?r"*<-ڑTugOx, eO߬4soyf{,JSD>;>H[ž/;ǪvZ:qI!npe<$W`m&ٍNքޟO]c 7 qvj HG>Z* SCDE"~"?f [`wu<}u^98,tLmJ_y%.[|Cӭv*&ԛa/:V56y.')bev vAO5b$|6%k3Fo:{VDI 3떒ƨ`ETW6 2́.-VH*aG"}Av4dԼ\#9*lC76!a') /OF>TչNdxܽ3~_|{1(ٟLXI9 e0mM=eA}ka4ZllBQ&6_Єn98muSˎ!{ p\LAR ,_ۖ|:EG^DQ%"l gzѿf_$D홢-oQP|ȥ4ZyHTm{ךVb'BU/D̬]NoMYp y0'9TY^xXȐbND\ޝRb{ s ^pֵ[49N\Coشa]+\>*9>.~Z61ю:5c qR!A6;bnXxުh.|t L'[ W6Ĵ MQZgu\L>ލ\ZtD/ˬ`3}TV+`T= U̅=m[0lذ%蘖^j#w$kֱՌNr`ȦC?ƋNFolaU; #.c3-^F*l AGxXG&[<>%j[ض6/v=90˫h( am^j$Ws.eLRiT෴ F$tՅ̱(}٠&O0WdzR T)F=Y ;lSrLVUKFc?oiLXc6 b8tH$;=0?LMSGeϾ|(>*C܄ NL]~?`}Gj?{;D1G MgHgQVk!&r6q>?Ϯ[3]SR(!Knݳ3kE%W?1A-4 d#"]L'_#Qf ZF›C\[*>X'"gnzl8["F}X7oYNI!^FHOxv߾VRvE꾮YbACIug'=+RU<ľH(.Tgy)9fy# I(d0Q*}nXߧzOfѶn(!AH c ++Sy z&2L`[ӿLYX)$,ة k;@G"Z/%"ȧ mrV2S¦(QPrЁF %xjc+}4X9c_q[@wr,QMjNt@&6@FQ#;gn#`/0,ՙ(T'aN_mBqI/S g7DS5§wM3 ŦsAN8_ .D]"ŷoك r:k K7ĹBhpȋ֓_~8E ? 9Ш̒<oX\ xP!d@2]n0Ϯa̩1 dALFcD&5XIUTt阷PhoΜ7lZ̅e+W"`vC^z!e&/ ~6kfR ?#`gcOΘ\f)bw<YZ8GZhq^!;q;޸OX19j~T:12sTuw {o2hMpdΎlp%i XRB\ҨWTwnv!){w8%)|R YYk l_XE/AjJIeCDL%$H#y v( Wꖜ&2y6rzAl'S=bCxS-wpĮRO'Ez?}&_/DRk: <ΰ(/F è28![vfTV'Ӧ] 1Fˡ̜}M1SOn2"RZgr`guy)ʇ3l@ r=5$z`2Rg1$ʝl5 wrWS9D5Cv@K`ڵe?6}/U}=XvZ@ϰi8|H-)aD!U?ߓ^q 344(G z뗘lti?{zyPxuK,6Foޕovf[ .ꁫcfȈw+4= {6޸ сQDN~T́7]1S6 ]X6$^XڭhDzWx(HjNƶ&(DdM7vΟ(Ek #Oh;GuPdue֖6VܗW,5P#jsk,;pI?9\ng,E9mjm]N=fY}Ukm71L#I:)[j =rw9ϼ%a*{y$NQ|aJ>ۿע2n5W12{7p6G~o$07 &+Cʄ[+Yz4M;. )nL> O~r ҖK}y] % }x6[; m2OEl.[!t 6ϙxsIGNYJ1ôq Tެ4&]w h)-ߐ 1PS?Rōu;6Hho)7Bʹt@*VVa<=Gƀj==/=Z5:e UT,NO7HmYCX]4 )9?"IYfqI)imwUOJ|Tqfxts~Jأ}ݖ ),a#8#S25SL"zEH;@j_gfꊂhP)%tea{w |,!lv_-~R %N'\M p k9B_ni6D`L~A%n8;/U)|. /.ώUzl'dyӿdedY$e(SSdy6)¾ Yxdt u U*kC]O,c,EB&g0|,=hZ7>$ D,)ݶ5nl ܹ`f: >PSM^CYjEGJ\5";30q[x;b6W:n^4̓ޜ-Q.|h@ں4uF$%VnԥصP6Ώpl N3kcnnScӮP#kHJ3c_ji1m{T?;BY3UAą )vEkaPE'(֌`fPz8&lzflb-`*3RE , $d3JSZ bȤ)gN TƢ[X:J:WTv6NJBl?\Y٬tn,q?KE]}̩Cf{JEƴ[T#~4J hxWRcq0[TC+}kƿHU5A%x6ܝ=.I&-7]~/J@glXDa`6n!a\ C3@W-dKrp7·X󃡌ߒr'Xnx?txs"LP 5!ASȄ56*"I+! :6!z?9]%Vx7lO,Iԧ-y_'5|ˀԸk{j 7Ti퟉m8SH,I ;g#i7YZV<]ݰ;\Ι{[YUnt3}&%ar'XjlhA#t (Ul'P}+zP"q ڔWdv XK<]4rRtc]x;?[{A-C=X~|[<תx gk3)C &|N+~~`.idX~=$%ص@s :~ ~,C8$w2CFt/2rMu"U; NݙCmZ *FŹcJx(Dˎ,MbCJ5İ!I9ŷmkogKsV/=6RZYZ.W3{zH'#SuW"8ba{)qxqM3;U=^.g1ARAO }.M9Q^)s3p_^xq&:Ce _s3zykxdq$hmK|&Ӱ7pA:2rbcˠԠj9\4tL``l裧5z?L7p"!JW@J} +1{m'*KSHF"ibqb9PϮtu=|9`֜lhb{yQO}=WnQHo+ ?A3B撉/OLl'͕9$6_DU0Tc!CTijs^DaxK6k2{BgM,xQAFϣSBw ;$?,'OLeҘAݬ H*|4i jb&_jةTzHǘ/C3M 8#Q/[#Ҷ7Z"lMށB_(# g<bRF-0|+nݶIik 퀛h} :jܜY=$6T |p/WGtnMȇ2_*bQljTqt ]4gdN]{IRGuB'^B@WĭPZVbpeI7O$O :mځv%j+L7ohûIv2S*0j%K fWGOz?YMHp螭M*' NY<뫽Z\iZ_`6iu(S Ny]H+柾nlf7$Ji:{c5W_kLlcX:tg7 v@ݍsf[!=jdpӖ-kXXQp>>Qyݼ-S`]nK&x>qt"2WMb܊GVAC"өD1T3~/rutc',`ᔻ,wWO%^ st jH;:b²?=%c.Sنf[wT\q̧Gh4Ѡ}|lR!Fm~&ڜݼ6Umb-4[ y:-֦cq/RIk&XvT, V85QɨQ z%?za¨}G 1 Q|+CgFzp)SO*#.RU1IdFR#+&L~U1k1}t&}彖ãS^6uኀNN CBvG#k~HL_,`6;5UFٛq3(#?ќJN{ȓH[ qlI.q|Uة$v?RFך_yx灃>,o;XWm_O ;Īc'm 䕓ލMlGo$X a$^<6fD\PG= P.M}Itu MX,׾];mgV6u3`W-5Jx "ᇬ&~X$!T [Ϗ=@}x!(w mJ(LK]H+\ L'X x ʰPء\nCG}h55 Xql% OO 6.@H:~,eѿ,\|8xTo5V`K6$vSP+aPQ9}:R?4}2vi. MKK褸#IF̻aWx~^rh!Z`S2Yi᲋6¬&E2o(DdQH'vn Kiuۨ`Y.>жz,hT`"̈|Ç/ m-23S~: hbRI3w$tiud5DT[k<"JuҜ48{*-e!Mc\M2jd\ QI+>Q_V-z]6"D􅊼|Ʞ{ @,ȁ27q`#Wz`Ia$?&6O|O`7ZC 0@b]719N"ㄓ-#S!;`6OݔK|M("ge40 RBBcx$7? 5b{ovyo3\eL;Xt'Q!cTΰ_թu},aQ>> Gɑ„L)`P 5 KͿaFFlI0maRgτRz;.{9~u@.?*.X1(f$=5k Cw8w%> GgLEєˑY2G~n ߽{4CKJ@>Wl W(Եr@5)0c+'_kX5u lbBշLe mֻ`*@+\jd bch1M'ICaUcě7 ]㊄>c2Uƍ(2k%a{AMG63 n5,(8u_bBbo 5IŌTki\Ecg_>+%y-"ԁLFш1HY?0ou=?{ U(\p2y`fؑI:"CmAOټ`tr?W.%v=$pTykY ?n'^u w^N@WJA<Z/^b]!;U9AU[X5K-UVih{=dEHe}ڥܷt_()0 tcho*A%Ќ25ONzi~eҁuw%)[ :QP56VbX M%+ YmiM4 "m ڵO:&N"TTb6jdTeR;Y/Dݦ.4%P"?VW_ Rxb40Zw I0zT @W+ub2"D!zKycqoDX@}eԗėݚ!1KΉ p`IDc׸+PXrWIA^-VQq`mrH">HxY}LfFO2{(rbOCvyYnM=JLqPc@=4c-6?`.~ _ zlXb&( 3 uUD<촾s ng 3[v #ǜ4?8rNu٫h:G8+:4BI3u)+P~5dSn\B8R6mUSgTZ.dY+o'z܈|Jd+lܭ<˟ɄIk:g4=V|R:$ݐ ^Zʶ{Bf=D9Aܖ=ٱ .wdlê? a[l1VYʵrDg-Y8@T7sE!%[ Fsr?kR3X߬Gv" xe Wֈ t\`zî9>:dAg Z,lKRc\g>t٘~M1.HIROxtea jܢ.<.՘Jd0zl+:yw_Q0F>WL70`np^T5r?zf>O1  F i]qCܮ )mWfS=hfqy% sEO(Me2 EC`YikAZ>fL8 c(&O+GnE"R'8Y1TZH&.ɏoPEv/ Ev]Ch]%F%Hz% 2 5%9s G2bugZo0$^f!/Vz-Yݘ|w)faG3lS iE9?]Nj,8?Mh(])uluލ4G*yYNzr{rQRsEup|9/YΩ3Bk&25#"D:OɡPircSn:Z,dk7!buÉDؿfD" KYi.sqXJ[x; Jb6B ΆSh(ZuihMaSdkV4ڿҗ m*R-i HIA="զv l k"J^tQ _r^@ >2@Q轆%Y >oHSSخ.=jzDˤ- VHԱ'D+BxupwjhC߁\w?S6EgeKgfwΪ= 4'@)]Nv<] u۵q;Zuss8#,J7a"b22PvϒOϊWʹ͒bA(R ӋZCd2rjbYCDt6I3H<՗Ibÿ t+׏<#$ۜzj-uyE m=(n–KħZ:5Nk|U]=bHO? IFOABFZY dQG8Yh\)8aUcM$yOxi)c5oB{o(p[5Sr9YWl5}]FqI۳7u~* ` WWן-vID/[PQ&޸!5M Uc\LnހM.o6gšt6SeSjI:oQPC_<}*+RtnLj:DY6% ä=I :wb,3Ğՙ[W6{as}d0(~ξi7892=ÜzMlBA\3'Jgw Ǯ|p$@afj3`9\ڭ]&"paQ 3IFuJe#cf|lXO VKf`gӊ+$٤ / P p%rUM3T0M@[P#vٗO/a⠐K@-J}R;gNTdpW U~aٞ#0k9 B^dk|Alrc'd$|72zJsS-\>6J]* J3SL P훎>Nn<`-AoiTnFV--s P!­h|mJDDZTA]Wˡ*hG߸&R= <ҴtNJ\}nz-I%=a#Cuڛ#]gaq=ch𱖴[N;̞l38!r>[<">'P%ZXp}yýV#vEV%!TNV=pw XE8vp߭FþǼXf1%<߳$uZ6ـeBӘ%qLceNx>cO1Y;jR'*n NlŮ&o&hUׅc-YL#T)~?Ë?Qr[{tڽ-B !6;t}gDH58ҡ.|\ԑڗ=]7&GhГM؉-5_P]v\q"XlQ5/1Lu&"}s ͓nj8bȗ;xgadtB~й؛3cj5pK9e V|<$PXJ'= ګH{Ghs ϩ40C?YXx k-xYc̞#=Ej9vl笻>OFϦ3HV8DjCm>Pwh_sPʞ}*d6EA1q1 - zq(ShJ)Bl=FII3_GQe$,1߮TMfݮiaV68x 0t:jAh?&e?TWv7>Ed3Y7pipcYVB2xԫi2nTG!cWa! &=ܯ?2K~+RX:^"J1QN 0*DV@RaݭVV +aD$ÒJb۾A3T )cDH*ĿbN_vmy0,諟&ABAsQ:O aXm?%RN^Kegiceμʂ7ż& 4}/N"hpJ~@VTx @J}KD-bV$] HxIwFW V^_̾ϧ-ѱA ߈ӿЮ7}ҴC"eϪ¨Qeu1p(5k;pteʷ!S/DX*.&J ,AJdoRb_կ3r;I>[?~No%ulqHhCړ{pmO 6]>OZ^@lykb"xvKZ'%)=7ay@,{CtԪ<;!QL9tNJ S@ nLC>i[P0 |E2~s9X Zz߫o]cc:o[6}B9$m?Nrm==,nS>'qlH82?q'UlWXKxV]BS2<ΥVc.;/WKnfLZ.Pc~w,MwT4E7pPh>CdݴԀPf!T༑ͤ=Uz$#5o*GV*xϖuR0?$[PQ4x8~1Ձ.DeD0 /xLz!5|`:f fU"^Ғv#⭔T}">茹&l"\B{1$ى8 -qVR+ݬ+B۬FIY@TXh"JAmݢiĈ[B}9-q^O6gyIShB}cA-# <4^f,Q\\42HqC.ވ4`:$z1as"1a~v'V HAe{BXם }6H=PiТm]d~,=vkiy4zlޙ3[ 8xwWG0c+xT~Lzst)-٫=G% oCVMqN ن"wH%'䱕x LhDO&̨dp9?$k}Jqzx"HȧN@aݺ62gv A, qYPP1P4?r@GXfaeB/ZFH%xI7bjʩEHh:Dj<²ki4q3 3s7eM~cxLl-D/]*CpS?N0\{ʹwquO6X0c7$Zl u,ʌjU+mm=㞴r.T)/ sa:oǁ!}*sJ#:jB],+m*,2M0-0pzZȧi,}4,eb}f6CWF"eWB*vTtGD8ڽͳ$QUߕAB~ ͩ3-h>CFU+?#ٝq)2CDU'w[m>LU4YG:@Qjҫu[UJLK;q\ 9rBN'ZzrNr?SD1o!_SͼLWJ%w *jz<Эh. V3cN_ :x9Yc~ި{h Q_ nsS|(MDrV$qCa킴]"%+t_c(C\SʋuA=?FP?ZѯA#;&" 3/qP%-3{~h+f\;,YdBf d-ؖ|*U!t[3Y&D/ s_A g&2$>%}_Xyh&2%yppob&O~0P"GIw-sl^6k#̦vKDIpr_Lbj)@ c4'a5فmCvpA$x\n{; t]/Hh__*m9թmt}Sje_~-}PXk*0uo4Y>OkP%|I$O0;jyr3s.8pPq@u,պR ׸*OcH; pO#0Y @rêۋ-/{kh6{W#rҊ8t~nR3\HZ$P8D\-~YjױLR .^ۡij~qcc\|C'qO$Ʃ7r 6Z.ɤjO4Dlw,m`-D)9զO 3n` 8"|3޻u앲 טlQkpdz]BW@r5ˋ_, vyxs0_:BZY@P=^\D$yH_r0v1 U"ދ؂H~_afW>i6dTVai6 t:b zs[z9 ETy6X}^~">(IEU_ii;zOT]:l<.^$:QІ-e#fRòMJA:G_`$glճcM% 1)z^~u@^JI> GZ[JE=AE%Z&AlChn|0d`\桎0i?dy-KǴoyz'6 @u["t 6XxX W躵!=vBbmlO,j|=I鹣sURq;{$dJrqy*# JhbZc0Or)?!&:/8G2𳴡qpDSeMOm/rWt=׎+zlfG0[߄##½:vL_,dO8q;&Wgt#gAR}G>ɭC􎅢8,`JIE*>7g #T*#kLܶ9naĀj>!?Ad y@wD^OG az4ǻoÿ]r} h\b6@b=,"ş;څ=0#{ɒLF'@kOO10v} 9\~?^4#Eq 4-@ʄ~Ǜ㎘@w (0-WUxZQ.0S19G'-%Znj P6bsgڤ1#ٷ[PT= m:6gJmrG]IǸhVwKfSoէbU*.uuA5h׌AɎRǣ*q‡\[:lr#XIQFF`3?cmZᅑZ +EƘW[Z|2H\}CDrٺ|)Ti BwȵF܋y8ru:'5j|e:+`{6Euę[b]OVt`!f7yGέU>z {{&[~9Cn p׳29V?„dh_} L[U\TەBbGHy[9AfZ,  6̗8jX$.+:G[Std4RLώS̢T[]YD4M_KG7E_"ƄY1ݼ]-j9['BRMMDIFrb'uD7k}-`kKB=¢|Gf"&vFGo% y" y?C m[}ub:9f$5@Jjc~1)R,?.DM)J=b8j{rS0~%L! c_67a^v[E[\Л.v~*dL0X_>+/Nig8s&nj0Hk3X_@6H,,sFJv!#X?vk+nϡ텿v3T/jpnA|y<}@ P})p_Eo+'H#fi7ѹpRE΃O2M8~;Wo>Dn敗 > *Ks%ضwT9 ^ңߞ2 TrK253Q_03hO=0rh5g4[C=|hrI-' ?M/VT ی/C&;q]l%2j)Z9D(݇:}Fr.x**.Tv#:Lj=>2 .l>r3S0X['"F9DPk\XyU=MN-gevX7gGqIa- vo7\:`4M@ݕ:'GH]db؈H!;P06H X(/ĨT\p8\ޝd mG b+E*vqRKDb4+ 3'ڍʉ@yo+ VDoŅH=Jr.g[_ "#M^F :a)b ҒRlސl:8\A \NG_7  .;YpA"XkAk? mpIc'}Nӳ;u5&+D@H)by֤l($<Г2-#̄I$]yW..7@ :aw\P0ܔԀlk1z $f_!Feˤގ`1c]-jT$3lU I.,v=8ąMn 8. n}ڸK/x魋*wG幖mYT㽖NEz$@T)'&R%VR~dQed./ګCMG [X}Y5۽ #ba;k gNJ+DI2p]1. xvc3 ;gD߽s/~@,3>e6 NlV@SFwϞl1p925̢tVn+ӑ% 9VF`:jK<݇w GK}49KWO6ܿm}o*Eej}V ^]/-BBdd WRdz!m} ju;(C vh%ziE^dSZ\lCZf(By%Qt+ AJLLTp]Ƹ<)!i;I:eB2faT{lQ%Eg(gOo. K1>5Y|ig?Dq+g=+ ?9;ML3kBYb@pxqd2q;?z$f}Q*V9o e6'κF箭s5RB{2I]d?aIC\G*-J\m0%`1 ia)j|=V!XP%h_T vy-#4~|QO G'+]Դ t\M-ТEĹK/N#懃 udF"D`䍌Ӌ"~No2mD^K~"d"du#mΫ%Rzx[6[_LZ PHe Kz조׿QFq`A=$Md M*AZ-NM\hHuH+|ӿMol+JM;!Mv}#Oy+iD`6i#KFԤbi'K+sJ]?-R+p|ҘCK0U_qNœtNc".vC~Wp #y> vsChdY槫mg[8O&3z~.CJ+p Hi@/~ǭo5b͝.< 9q?sDw[v1&ѵճLi% {#A4#9׀n1)2AJl"-D _۞{@Y Qm/^q642_Obkf|cmMj4ZX//՞`B>SD]܋}` hB!|IK4HoFQ3ɳ|EKfh02w74{ e~:]|G)Rs*) 69z%V, /ON'!˿ B Ltx$upQ?թ8{]OcSPfcp o4'~n[t?_'tJc8e0ӳ%|KPSۣM9qCrś#5@^H$賺>#ZY?$ aB(*u]0Xݏ_SɇA 5 V4P d*)'{ͪ*IQ܍uRJo& #[Dڱ2Ӽ%rz n HQc)jp]\sOZx=D&D{3۝P m REA~ѬB}Rb[V`V_眈zǀOO<+0نB ^/%w ǹ(̠qEY 4Lܝwʴ3EMx"M+ >f> M'2G`#%$9xeC8G)%Գ_cRppJ0\kEm"i)!2d?0 AaͳUqk|XS2wt [vu̘۠Q^?,xz|i#/@s₷u, T>d $ΖTC#2ѥ\塷P"3?( (}IiLc;.2 l\lAټgC$OnrLGb~܎$+]ޠ^(:$s[#6jaN ")œ-to0V=B 3k #X$d:sԄ9S [4^%C-GaB# d+ľςb{@ńrz/GW gZn)O:{Љ#&FR股v! eu<C6)5%G=9@vDDqa7/?Ie9u@߱{,}y\H{Eb=<݉{w—w%sb@eS>?I^hDVP̹^9oIOT] G?GΎ"oE5\zxjϐ 5mir@P&GYmϋ10֠hq tuMX93S[iw8* eȼ.Gdtw 3zOȂ̗4o*2nH|qA2MDn杖%d>$0@ q 'мDO=/` 59PkqfJ9oKs 4[}haZF 9A-uFs%-߱h.^MqͰ5!rU{ZgCoŘWwjɕj6l5# T9(Ht"Kj΃nMNבvGp6% ^ JTQd/sFVQWAŒ* +TNku7= &2R`ށ-ە{S 9󼩢A vg;ȵ%S"OJ=h`qՒs’D]tfL*In]qTRuNCxkç-mIw늖)^NGm%`$ Nn&iOU{G^~WSDA+{[3-&г(jp\N-e"R .ɶ܀okTtdq+H/67a~^}+i/T Dt~h~\IBnwB6]iNzHj֩wp#ΤD; ϻJR-ZM=Kb u 0ᐞՔg^>ԝq?{1P!i27 0p D_>"-Hh/1}֤D BbdVd!<$أmrW>B9nYX5JR؇qPS\k$wP|E2m0Ԇ?导/*jb.7ZɆߗfI7IHHVw=$Tg/ MXOO#_Ęnѷӎ5"z]R'&6+L\CQQkBPF%D1EX,~1jx{QQ(ld P hU9ɲyj 갹W>]Uk_ 6Yvk3VReOlE:__Ճk|N̊ spZAմCaC.<k,U̻5'fLJ9 7Ч^R#q=M/e#_Xg$v6Qfpɱ Tk 2J_GoCK)?:"XF杰u11æRq_$:]۪b %?&G@Fr&2Eqxb949+~,#:܋|EI|,P`=ˑ;cv3q/;s}ur{.@b9']2>?w< ۵_l-)D!>Vsa!Cxvs9zKԐ=*w{쵌xGyzqj {OALEJʃ?R5x0-OR$K7 dz3=?/eʀh֭춤;`REGEoP7$(g\mi:[Y(m҅s>s'/F#-n=3oXO50(aQ_=7k葍=kJID^]ccNΰo dx]einKSg@bb'(/@O$s֔ FcC\>ۘ;OC>̀K ^j'L~BAL#8UԮ FLrl[ʜ?h+Ms@M2wg> Y'S[iŀJ9'&P,R d LoC*pEn@Y=oRfPىTuD9Kݭ *I,:VIHQn ruӓ]m>WAX_Ҕe_Jd};YEbwIHRЗHlrOc7㴍]!co)1w(#}9,޶@"yK iXם Yk 4is.7ML2< g.wIZJrΨ7$笠@ݸpa47f-gR;;j~Fφ%.IҽӍYk,My% •;D՚$9X[ >;>Ƶ h3OQ՘#D%V* r*u9=#>%"nK&BN_fe$Wٙ%,Zn̚fت~mv%D 3B;}WIuWLP?aƙݽLݝA/稙$4Lr& K ;%M9"tDL JӋ72i yC2WC 97)ŶoHȘ5טġ\ec4+M1$;Zkq 7.Җ;{b0S Ϗ~^X7 fv0PoJ wBB\lWFD!JƚLJ J]t$k>7y++z*%{"G(|d|>SJK{{y>H iDh|`dj/.*]ACvjtڪmY{.ir[J^\,w j}+<4˴6c]ͮnUUN9@XT;oA]$_cAY:xdB!0[ʪma{ett2YF*.eiL^ɐRYR:"jkUFd%c)eKڳ"3{VuYo4vh%%|?W-5Forj".SDb^}TtH. vÒX4\!F1eއQ2( :˷QR:ȹnX9M⇇<IY{?3$i7AA8UƂ7KnPT>|8*5f/iiXK#.b0dՀ5-DnEт&< H˭#0r>?m "Ť6T 1 (${efZ>q_-3bK߲??0 Kc#H.?g$jصvI~Y0=Nk)L`%LhlcH*fm 6ϽtRZKń ~&94C,lUF N1AXx%^k-5-# ,sq oՌDCM\ǶW,y] [/."{*F\M'{:Z-|RRcce>v6tsMXO3JR&J. ãt*ÔLhLcig!ztlA~/ 4)ʽ%I-kI+/ZWvσBYWȦ@S ֩WuGc d0kW&2Ÿ8Tw 1&^Tr4f D:EgzV%APȍ4p߹3Jd!P8 ^ ^#KFU l=*)=,^P-XD:vժ| ILʅ@;_j7i5ƤN"~␎D'>-=c+,v6r]-es)WL} `Ky[h5 apPW-"HfvninǰIg)V=V[YHӫs2izNpre<fz:"OpP2mB;_iql7eWW$Lta?ypb30n)SbU+k7N`_cMyHi,jqDa}n^RIo;ԎAAma@b< 4?<([h1nz| 1B) FY5foH; Dyf 2bBFEdT$) 8`R^k[Ht/l&S2YnyMWSju_sS|\մSΨȘ0#~ l͂/xRZ{C+6ْpv~嫼jA* HW/J٩V 6;fGNZTr;å38Bo0, <03T\DIMu;zXGgq8Aٺ>?)*O^ G4HIf]5p 0a$D ]bg bڜ=|& ?gϔ߅y1{m耫$ kأ|B[) 6N_tL{g/~|\=P/j:9}ٞ.=•4{eeH/ m b?Wc趠h9tKp6899569z+RHI 'k$LdD8ˣ`r"vz-:6Rkێ^ o#Æ=ofN5k5d<9, |!Əaè<o7o:F;7>XnRNBAch0AVS>{nOfّ )X8)ޕ2% ϋϟV_w<ەOݩ; Uzo#m4>̶d *WՒ@-ܷ & 4]AXxp(VBXy> IMN#B䇎3VJlpY#Hj^DR#H3M}&S {Τ`Cr]ɽv΁\0Xd,ErH WYhH^dm?yrO4}ۦf Y}#WBuT.ROm zz,Pjl# ) E K.y&޴Z ٰ߭Vm<&?l{5JD/R_r:+>w1%`ZQ?^]UW:͈bhq,vNZXKyz.qmCWɻ?zRB P˭}] ?iiȌxw7-cbHjjx`)בzjgj >VQ*5\#fHtRD$Y 8X.sjVa /#: }]qTwE#P/j28B{Gjs,7Uj[Q=b.1?s]w8_&ŸY+$HYPU ֬$}UL!i n֩T'P[`ju9/+ [4pnC5JTsuzOaT'^ IXgDF&ga5$'ѝrL~{u2Qd$DK2q9JGxǪ-od/&RQn6YΞCk#-:zVCPfwDS7XB쎋?)a?`RP"p''ܙe19Ǧf$]o^@sдlmew0ivq*|IXQj74b/(۳mAGK[%aD/&(8l2k ^w ~(+B y%@DY#y&1JB F`t8l4B6rCd퀈 dxtƷ㷑n_ڴ8Ai6H[<ҴO^bLy?Ҽ:6-7, |@&ONJ?Ya΀m<8|W6>ߜZ@5B u$,/z+}Dрex])sqy 5;""'PӜ~F»u"/E޺ @H,Cӵ3u3B5w΋E)`2Ϥ4Y.g |{:f9ElSsnTFmdN[[!SrB<q}_-EؙeQՇ6~"9G!3R9&*#<5˿Ms3ruF^<ꜗ4!w?Bq%(ޝŁ׆t7׻3͡R 0 > k]pOnM@: ܡ_4h[OQ+hY t\5K%{"ej.vydI\'fxi{}C(,vd.ϤR Cr |(ȔႠ:͎)<|I^?ܘ;n_jqU$np 3srZJEmSDF 1K, IdeVPh0'=Ԋd,$µ,N&QQ[ [XdUHfL|o_ߎ5@}OE 6,`nͱN|5qYo~i՘hjAӗpߦlX ((iH`]T&MzN\Suc;$ݱBT9 .k #ǒY:R5ϛ2DLh{qLfWTwD j.Kk䛕|Btf Y2kz]I3-\ /wfg yn (OZz1;Q#M< Oݰ3ToOoJ&%enb\Ƨ5f.Y4cGT:ភ94sp>xXWi v :Ŗ ݮT5]t )*{},4ja#S'''0 % 2pl*p^<"a,?n,NjoV κb/ Sl YL ;ψ5ݫ̹b͞ϾwVn$N2E$( BВqr̬iiwoQg(քhCC.kE$:ՈCɞ[l9ߌ?6=A2AcO:V89.AQX{7?1/B#5L'7ˁmg8iX,FAsX)0/:X8n:tw<j'>U1,q+ĭ~ӞH%?EGF9C&/ U+_OyUOש_6Q(ܟ3.0>I+Y/*[ b?$$DJjakz&U#;tKj BTſkF*Q{߽Qh f,l$;AK܊e"i?bUhR`×I-̺I`Bl/W;֫+$JUOx6o)1{QPxVjyW=]tnġ\|qNYx*[+Ю߉o`2>w,9$mYH 9>\&yPVTݶײS_EթMSWޭRpٍ'8x1 ۋA/H4]hR d,I{ɒ<$&4E.4#Դ\bdlqǶ.\Bp4[] UR{{ y?L`F2BR FcZn ""9Ţ#i#W:-cnpJ6Ez3 ǂ8.3JlڮQnX#P24yꏈWԟm{8yb`xpuJ4b@t44ũn)xC:銗;j9CP,W!lMS 7\EΙIcF5|h8VL@˛價O-`Ypˡх\WM;~!z$ ӄI!8dai8yŠhczta8>A@@dG0†T,a,)nJ 7?4&Yʈ^wې|OAH˱{41&xE)jGg+5^,VFߡu+ Nϲo@m32Ng5Ǜcg ~ui2 cB7{p=}׉=1.|;)J7hB&_ZT#Z?"|bmPdpq姠!zњyU$NnOc@w/HȎsl hVٮŏP (&%jYX ~2 lϖ"CD!![ fʽ%)dʹV7E\XOW``'@它~ v Z'Ъ@7xmFP[\=ph#eL%3~}fZ Q$l“ي?WvS1~qBUBb|i̮@TpbU AvnBTVdf%"'E2E5)3*bYƝ$eC dEȢdti%Em[2AP$4k0G@Z=&4#BDH&XF8n+.; F:5*x䃀 AIQ2왘zQWV=19C RMNrRbtD{ >$yn`j^JW3acu˸1T3O0>~DqOؿEO-\K~9.NRRKD'z)EL?s-U u[671Ln"t ?1йω] c{ݏvI80LfW-0k򬺓l4F^m(>.vR:4"'N8"}+ < }h>:Wg JMCC&2vF >X̎ڹ̇jSv<A,uH1w^WQ9U D; %ٷh$=yU-D*.46'F4Q8r=&PC$! sP=٫w-B #Cd4ņ/K PڮūcQKΓ~@QC#y/JܺS8nס{J+ tǶ-f^dՖj½@ ߓf+9[ _y<:V#D P;a7?{A,(<<^,q!{nwX|W&uO_Ef:[/>pMȭDP|Ur#Kdlh74[BQI8/a2x_҄2=1e:7ޞmYAكE %1i:lld.Ve&&M33:Pd!ѥ xS˔֎ײ16YqRGuw sP4ދ`&c fڴ~B\JHa܊p;u09.ۡ3? GR!<'QvGQ:ZU9)ŔwdQiV_+=U6'_af#rKaM{!Gzil:|~rZ/kuUt{w=9v*荮F!g)vb+ȡL@1žSor2؉G9N"Bx\i <7JlCJ܁0)] WmW)q -*^/h3U2L>&|s z\ yX~^l<1oXc9pb'tR+vIVI|{UH.例 fLXtwl]q3s:Aɋ _.Wų _7@cjN-Om.4VC=hS8- ˣiTыL[ P #txS^DŪއGQ@":|-N&k1äĤW轊g)P HO("VPegY2  *c9 5dxqj9,4@x0.בNU7@F0,Lϥ߽(YU "78N"%o;7?+&v۔[6(x 7[!o>2eRxOʥ# *ŶF|va+d, !ދBb@$vO)\y/,0 ?LtATcwۀv%džK,5\Ct%Ɗc[#_bq=M}}n5P`Ř-{$_ ~sL1 BЌ觐Qƨ6C:[Ƅdl'aXĨC϶vMjMMrEDH]h Y+'{?*2LTlBANUwbQw Gչq-,ʟ.;VAĖTYOl6er0p:yBTs5D E]Cv7bBY\ޯ)Dh|=+pqAt; }dE^6`̶e睸cP g`|4?JAy ( |N蕛d2 n=?d`Nbb.XO R _&DFb*!6]unm|RB'X>HaΖryi>eL+Q +j?2-1x &h0$@7, ]w~1?}qd/'*`aJ]CrT)A.ٷgǡt5U|4G-if:@5cU$۝@wAXB~>ں=< 8,dee(!ٓ?x L.ϸX,vqC dH)NL6Aw џ߰"2z7X).̠gLtdYʴѦyI? j 01^MY!6|Na~oW1Ӵ 3"Q:}L0Wmj|NشSw ט[b Mϑ0@W, YmԎ'JQ`3,~Ad JWWdj*TRnz=wo8!WI:ޖfW 53I!3(^am>iC48ēX^珚9[ A l6+X8pLZ%Gݚ߳Crp7ΌoS5Fxy ſ ba;\[wO -(bZoP`}"+Y3a OPV+돰9󪏾r v!f҅xto(gU[́} i[.aO 'Xݞ"JzK^[.Շκxy'~:1FΊ{imhBMH_}>?SHl <,Ck^nr[ R>۴5s <fbB?2t0gg% T\[|~e$lp 䘿~*stbe1-R?rmW i7Qۄ@R>FF;S뤛Gw Y7ۏ?6Aw51bh-C|Q]-r^]STh#|CEWE >K5>BWc'oTtJeQ19Ɉԝs,/$R+r'RԤqL꟠w݄hNrα; ADЁƴ|ƀ&:E<uF4r g*}⡢`T4f KY['J pj2V75PdvǸ5Zk* iț5eJ0W 7tș\i6J9D!m\wA@OELW#Bꃟ0ފ\G"k)M*\R6;stJQ?OP5MZL}z!93Eo2\jЉ/`}v?Aq2ͻps\"$ #fp 0m%˳faDp^ڒ5A8)M #)$Ԩy"f] ѱ[g6⍲FtDTyo5bR $C%ۙT;E<=z`3I|zzk`v%y0=Oy v2 9e3kNBjP8|m5 Yp%s dv1s|M\CK+ J3 اV· )gl ^dҫ S?N a={1M۲zUogEATEVK}c.m6U^!2`ݡ7luXph>b|+"3 1Xd C2D!y 3܈qIF@и*F|r4 7[!CKs$hsL1IgXrQ8 S{|E:?k5GC3 dZݬL&T'u.?o{3xdl`KKgHR6K|$mL,s7k.fɪa5 e w;5-ݗz¨()C +^UON}'C8;`.2p#دB ;$qMЃ DQVFOsّY F@k]LϑQ;*Z␫iz-pHT&}sJHy]ְiF&q7Z5vFݭZ7ٺ]' k3RiAA5ɯ+׹KNlI SRY+4:;.=I m 񍇤* mKQ5q0eQlhݪuO#y.~P.t >bH ֔lW^w8n:ۿҚ0~-CWq if&1y2MGi}ԠLJ>yϗ4I5E.Z4LC/FɸYՃY1-S?&*lvm[ohmg!sI+avqsN&lO|NgjTuCp ]@.|LNPFywXJj$փ`fa/Ӛh[|A1-%v2ZPJj-CS7tuD% O@Oln9Ɉżf 7SL]Q;j+8~ΗdY8Gb;]6047IGҩ@/j+ . sbUi\_mquҺs#'r+MPoe)~V_"$:02'FHXE)18C,Nl^Q&k!EW0{Ic$JBrݹ t euk414fV6^+軦$ BpNƦrΠKH•\nN̊fşj^HlvP@X8>0L;ir}aĽ'.]X *0l6!O \Ttw¼D]\7-{:;&E8M952v7s4!Ԏ1+^rJ*k&VC ? ZhXےE"J`CD>DD^4M=$\gu@")yUR7ɰ!{4i~fMes=y*N?B6ny'!-Y}[Yo2BeIە,?Q؋-@uT>ʲz-0-N,B Sk ګlI bh`) WCWiq_bRG>oG޼JV-m@M ;}.*7'SZ/lE[} N`7_j9UR\ke}m虳 qⶶ04KD '2dgX+5w%KI00fdL-:;{v:ha75Ycz9ݰRfc Bӥ.՞!T$F71 >ھ%F͉Zxұ->GU.Yķs&U 9 a5yKU/erĝ鷑M/^,[,L'Dj/gsmb&bzxuӬ=C[rxӾˆ|h oa|ؑZ!ߤwr$ڀqj8M]tN5~]me:&3o @/i?{ 0uabĻI`Qڅ{jcXEi+] 7eo&szV/N/3OWz٩EԄ72UױZ~*h'g &/x>dR1Qްn#ozPI!3FNjq,l䄙qF`k=lC:C 9G"kMùⱙR⦬ *o?SЁcyX/AxEZ.z{n~7{^Gg"`[Ѡp]=+$2'T]ጜtda = S7Od}7< iiXsThP:^R oD_|}(:\uN[ QƼ] w vխWbP<^}Ez,Ҕr!4e  pFIʼnhk]OF4\)@~5\**6v妬sڔYgnbN5gC(jjM@,BE9u,οǍ1> J .T *en,~`:1wr Ni)o˵oF"n՛jQUUn{fO=SxݙW:غyxY,82ՑlX:ݟ"`-j?hiu#{Oç_~s|1 A)f~fR6  `g~J GF*>"ؒHZeǧwK, Sajp}w$_`/:G-_) _Y+r4ښmgYŞ' Un ? ϶F7)W^'˲,> &kɇ=`#*̝Sl06:M-Ha:joEu D'n?5b+L6l2' ɸCl xY~~6I(]as`1`\B.q)MFf2|MK(a@Vepj/T0@Pfs x<"+ p7-8V/YCҎG OoIy p *a&P̭@oR_gQQ{=+dӯo(ld@x}OE ub[} Fɿ&3pZxO!<]wB)=<ծ!:3Gީt'+wɰ+\X;wv-$Ky}NL5TwEK~Em bZM 7SL`! S)huFi$VC`+л >2nĄ=Rb_8#ԉ/2A~>ҿR 8h]7(i ޷"c0縁pI7i(jj#d}qo?p8%Ο4<)ʦK;3c@s5#nJxYB&!PAGZ1q 'FP5ʜ(3꼲g>]WMꟀhLY`k o62Ɵ'Quݝr]ľT$U5}=pÚNDFN[Gs$ Eޒ&JS;WDoG))stPC%J 6| j2V6=fQ$W3X&tVSjujjKriRĭurK3,d$'gCX@ et<ޯ$x9v*GUA=͑WuG,9i!ǙL0ŸDjP v읷f,F=rFuŐE\@Ut6'1V,uBtě>0m5? 0-O0wEJk.6L6ZgiF8vzasD&a &(pěc7 Gؗ] |L2?h^ %9[`,'AANxju#肛}Ė̦{ͅ+F­&:\%Yb-ds^ˡ&nM|3LJyN7lQ Xc=T |*r{+_r{'8\@AxuVwR٪sc02\FҘjhV _:8ٮ4XYa;)z ؑ233v}^0-Fu 㥥8zuGfsR/gV@]u)Uq}Wζov2k-@wOǪAyJ]uqܝPzglj=w? D-~?D:כ%(Pa8x!ZJydAE[ҴGB7;qpJNtɀ1{9E?QUB mb}T}_ {=fH8 ӆs ߻7c Н6PC^!]X39J4_׉ME`)Rݪ{ kp}k<-^D'+,D=@[ҬBu]( IC`Zp$3Q Ԭ]˫ClS}GI,J 8Ug+p&ruvvsu\pPݵ)!Ѝ8ҝHXjBFUi7%Hxw}iF243˱{b434R_)Į }~Y(YOu{ ,XfMu.I2-ƻk.*q/iqv;Ȅ/Za ULbYA]#3JTR?XJ:C?MZ"J#D㷊W3.rV$Kz=LlAQPNx!\,t}tQԔ_5:=]`^%ݛ䕐v,L4%j;x۱J!uV{bڕY\kE*zF%%UtVk_[0[Zz' @^ 07;N"֭.ϣ(9hO8M15`y& IoYOރ4,:biW[ WBu~KN"$9YU 6(Dv,6s$#e<|L $.-/|lyjJC2EmlAL{j#!#} 3, ࡅٮa}`ζ9ju;P)?G4 @]$6{Lq=&} ; QJ0&g!H|ᆙDpM vXw.~:+P""/Ftȩi{͊ojW'>yt%Z0d,4th26]wHm&fDN~+UlEkV3`ğΣ{ڂcoA,)*T8(QI~lȋH{ '&"@=p hHȲJf<ɊBl;23(nڽh_Q;3UIycO*)1b<r=C!$ aUح6:,խzOwՈ=)"ݻR!7OLpw|ff+{BI㹠驪<ol[UF(2h^9Vew;X ܈*Ƅ%wǸΕTD!nxܔ X_T~Qf;g3Ǭ0p@K,iPeKRu)i8'sq-8]݌k<'#DRۡCa*Լ5!:9{ j-\9w,zfBud]b!wuLPNg3 l"0f_~##.iRtB!w_,2A%O[+J%x&H/&:MUN'}3m"0&-K@ NfîzNe*XPyye7ZE!Ne =^ɀxu,6JlB?-.4Ѯ )O [lmѭ[Gt?hÔS9Lnu_o+;TRa5Y.3eS%EmH7LXEw*OAYBkO@~9Hֽ-)_26"tkiN~m0~+1dZ7GE`$$E`0BJrqʵ5(=.lD|BkM`opQҗn~e~H? rJ.UޓZԝȝ4бڃ :,Y)|T xQػHƿJnQL]&GM~ez#jج2 KV- T ~1ƪ^r\"s h[CO 0YiCK"|2w4믑 Mf>ƺ&5*ȢY2/= P3J}i}aa_-ք(VUYo Yd~~ r"msw@Xi p|l''Y9݉\w b% \BnFLk,q֡ð)dm؈b+zY%t+ҽoݭNn7$MdbYrtF '"x<*] >W $NV~4BZgՂ-(RbI1<>$ ,x0jB)٣ ryήIw$BxN&7p,А4d2,Slr@tf!7၅zH(.uτ +3Η;>U4T=fuoh%L+T 9X)%DZW^p/<!Í$\Mq5 ikPyOrOk[|i29*{5%'^˹s2▃^Rc2?Y.F,әN}SK|8R<jd=6o0R]ΡLlO>" ^1E4 :3}f]f-5fyCUs.UGCү-$m[~ !od^HuqWg}LWuE*leoSĹ0[X2ǗgnJpt.scU-}MZܔFgvoHE\Kh% B)+Q_Mʈtu E"g~0b.x70 nк*u?J@:i"1r_B%闃_9,e@ч.-' a>ӹ X?VH; PX(Rn#$qm-efI\9"^耵Zp:g)wZ_Ӷ)>f%wV#kDG۟%JϘT$@,k<(Xg$U҇2,RC!jTc)2-{ ,5ʣyYx+^t%;ӈGޝ4`Y&!o pAW&*]T\W_σe>ӣXN/M'@*5Eg{ . H2R_!豑 ffs!\ c쇊zwL_(x4`}wߐҔ6,ZI|֯r|0N({F8ޛ̏Hwta!P P,(<0^Ѱw%v>BqJ31'MC4Z"S;+{AEGafKEvC:YLEvE*Ak #Yaw3XXyU -  yKW^~np='׃jևo]oG{{V%edpwJRc"ـoo){|Ly qԶ| יkmTjʪ7QQfCg&432z/SiGStaBNxAʶ'z)b#`2! oyʘ hgQ#׹,i8"}ݐW2ԃkr0a(t"=`Hyyeȃ~{[蝠c)j'\2":4Z7[3'lb,vY#=[Krf/ K#8|&PeI"AX kiET&k)T 3Ձ卹, ʠfW0E԰g~S>j,B: \K1.fuWƠɼ+p["@7oK fQt| O< Ry:ѬOU@tx)5'|$/TCl |ڋcG s6;}rǙ},W=3@~Qbn>㾨KѾ n4NB)1eWڦ Y %ÛF[8 myQV|uDQpC)%ލQzjA(t.&W:h)u4vQPGg"͞Vw m'1vI0;4?Eqɥ1s?:MiTM,w_Amf9.:6Pp 08P g/'l6ZeG׶207g'YR| ;+!Zوw?e/WLGD:51BJLg(+H2#JT>O 27H=RQcT\R,M-0sT+YK; !Oҵ>üq2P1i*<w@3LQ^@)y@p {L~zMWքo=X#P8⤒>t4AuߔV Ǐ:E(kL*ࣘ`!KۘXJqB 5 9k2:8uibvjj`ʬ cWK:e9jV5vQMIE*~xFoF&ڡifí-|ISo-KwSĬ%u|g8a'O('Q"m?7 M"` gFķ`cvqt+"3AMIm$t9ok (AVw8g-` ]WK%{Mg_UFEoC]U 3f בs|n/Fņ›$ 3AC !K8CZ)(q !zz& "9&' %)&tԡ1udqr$fq\-޴3%l8\@yCƧ&wTΒc5FKp%A5woU\BpC >W1zfkt'yM rqv!\z_dprpZ"GƑny]XA:{?YcdArάƣ) 5(4);Vo(u&op08+K\mEp53|uvEF/C)!6K mCeP ,%'pK#eMsEohUC`&v5̠XޡPmz=?r䇊k9eH)aP:&n dzS`g߆P!ܵ{BcQy% r1)3A)A_92W{Qˍ`=|tV/ Zޤv)n6\[i><šТU* 髄Rvg 2 bz[EG%ͪ6H_| 3;׶+k/NioZay +M0urmlث>ۆ~L9 ԚQqi6Oq3&8ω3JYAL[+ÆA3]<1~ H2;^&l:uɁKg$ZǝuVN^G۾mZvA)$ oأyt:C!{-HuAQ ?pv:AHF^@sUS=>/ ^z*\ٸx|"56m" PwK ,q7)"+G6[1AfX0gLV~ܹD7}LB^0PiFF%@AoPUZ^^Gm׎QIF ub|z9* 2rD&u3cۻn|r Y{p3@ܑrScɽmN[UB9]M`lՒk+ _"zCȇ<,Brڞ YKΥy`;zBˡ+&JIE} /:ģ\V.ᏝŎS9m.'up`gZ^9]#ezQw(2k4ι.Uڮ}IlQ8]C*0N~' c<`_;u5ag,eA7Z4$zӦ(KC(մx_Mc6oзz-6Fpгj 8$z6M3if9/{ 1̕Qդz$N5Lw^*(3R p;L9csj*U:6yRYw@NS5i;'o<&d*&xh/VOK-V][4|@WG?z[Ŀ337G)i|^'0@.vsNN]z? :JG`i?i#NX/TdK2$䭷i3xOeT?B$qzoV곍# GkfҾe?U/fy 6+;҈ pW<Y6<{uu|T<J[ܹm kgUӧ| 2t /L8.[&R]+l∁"`i:.M+ls3pqզ%u> sT$Ӵp؀[l"p0>Qdr`G)*tyϯ`?lVHm}{ m`j/{o`J)ƹ1k1@EDԽ 1AANvG*ScG@hSuӚ! B]{z`ӷs$H8Y <8=-.5%1l-D™*~էGtA51j߈Oj;:":eLpz3WpDd;ŢG5+ypos+O1f,S r1D CHlՖFҘWBM 4\iZ;\|N_s^%qj4_[0Ӆݻ@hT?ڐ$md!Sҋ=SLYK)L& v*sa84~WnO>itO%\QXt~B~hkI;Zq2"V釂yz; y]Be+kj ݵEM: FJj,'9=R?BHKL8z}O蛡 F4&eNµQZ톾ahʞνYaBt(Ĕee0@mq,ƥ' 㝡f N뫞T EdfLbc٘rs-H;Y[u)plRsiըO͔UYx\;FdFVڇ;ln[GLq[+{G^ }d[ JMÆⴞ3 GBC4#V3*NZt)iٰ}! Bk֓[eÄK6pÕ 7ȣx6fF3v<TN+i7[;n.| d>Md}Rf_9촅~Cx!%1NAҪ|:-Kf[5tn:_0 Fd}` <;p>K{ jF7w1{x0q@n9-&hVilԕ gHq]U"@E|UmSU8 E0ѷ.,w δ>˩5\,]K̸l}3H<ݟUj:7V&SўY%NF8g-%4]WTv9g؀,l/ fck P#u{j){d5%-b sgJ|#aGP&t1 gd|fo#H˵.  *=Y̓ ȕ U16{]u\|d }}M+ڶӳcLJDRnJM w<&OE賀L{*om'P.릘ڎ!% &ḚSn<^"ݬő New chz߬gTytIPu KpQ惰Dryɮ0gUa47R$:fcUXK8!z?h+{8lLWhoiw3_~kg <;4&}"fdz~/Z+?_mF*P0S7+aVt637ztP8f-ޢ{B͚ڀW( <Yq T"l't#rёwmw;00'}Ͱ/:swg?\\4z.!)&Ea (l]^H:lv=лɠ /RDHrK0nzCxJesZSa.έ^0L( .'F<wjP3t[܅OJ4;IJ 9-@(//r(F? T zV8XTZUjhx=> #f$m-ao^B-> _|{zp pS )J 2~<4ۡ{D@2ڜÆ/ eXb\x(R0~PrrϐvsxQ0tNbXDD!Uޏ16p"%I WYW@ mH{˹VH}\eDm)jF$uҒi 6Zǯ7}ďì[X9߹:؂3 J!:3)Gaքg)WpWbV @-Z Gyw-TYɤk9.'Ҋmagpe`,S uu1n`DN[ؘ:ya(y %d,K <.5Ӈ߉ sd' QsNEZq Xdk +YW̓m9ϮeÃ^.i;2k7Ci5V~G&N'v*a:x4]u<6 >&&uWv9q׿Hk`INsx { ɺœlBs׆9R~Ad<,'"R yaQ)6BlwH0/ml6>_plNi'$$RJ33C14'>тM>ӗDgu\%T+ԻHԧ#M jL6$$$fu;MjR͘t ]4es6#GtO3)! ̶mqp~ig0`dqA]uɑ(R(08:{c%sO6@-dN/ G"RjД}|tnG!UhJcq|+ gW<|ŗkwMI`b[ڒY Š˗IMUIGRvZ}ƏTv\Fe祸fzŞ{:dO2A欌)*zEPj3Fq e🿘 c -;8yg?SVm3]Z~Cr<mhXbF!G?m!Hi]ȖwoGΊq|I 81>BDc%*U:(P Y5^udJofKfX~̮͜y-|Dnz͛#L-PSV /z|L --:74*4 䪌|C_,wkժ_ApWI_zϺI8˿ !yn7* =eU kS=TF1dN!/2ߋA?kIܿTs”4s}[IJ%iyn+p\KMarl Uߡ̏yZ-vaM@#Õz9:G+E$mwz?ζ cnSVg,,tca LyuȨ GPGT!&{>q;])h\XIkN<^d՛,w]9m rb΀װ|[]{Ңc.@e|'K%~;?-c6O{Ԕចk"oV ʵ]G7hC0Bޔ#b,jkQE!Qq1j: f< :y;f4KؼVTVuF7==ȶ<@* 5Teõ'&%BW !aKD\M˖ْt߷|S+ :Gxt* ySpjbCB\92\wc1L_4H%߱Fr5?}_{\ 5kHooK0-D75 JOh، C"[!fd::8Hg  D@kOnm}L m(!/ewvh!S ةdw%;{4'ӟTpeo{+F7v v:9{i!긔L'^zE}uO.=g)l+6D k)G!TK)eS^!!}Xa~Rd]_'^@fp=Z5JOXQiy`\rȭ 9OiH|=C% +*DȧI3W}][^ӑ鮓_C=ݠsH~x`Nq PoQQ;:L9+@V~[7kο><C괗wȠ~ ޞ X Ϻ(esM.阙`GmzXϤA*0Ran; ;鑟*ZmJTb x={"%X_ ?uHdpºfv"G]ξàUY'V#~CM/(T3j;asғ_gRJ1 g\j.mb3Ī[HGH:~8*ZC(O4Rh91dBXlSBvVB4XY+DrC21 ,@Gf3 l;jH#Y਼"99PEy{gΏ򌦽\7vXnPz}q4Y_).fȌaI!whQ$onb_qc&=ʴ #V@ Uܠ&}"tx > hf0MO.BVA){?+GGs "%ڣ Er}L? #e0qtڬ <G܎.<+>+h5 o$0Nm%nl/ (K;q>;Pv2fD{{aJgw$&X!_}k?sϯ)Oչc]ׄ3O'(wooTe?Ov ,fr ?";2ӴpyKrPH&Boqgį,|0GɚG%|dF?zR܈OvpO8v8wx$:qm&2jJ & v2TGq02H@4 &}!'ciw:a8j;?oI;ZWvAa 2U!vR(;J$ ?^k?RP͋N)`xAH ܑ3-hYsQ5J\"omTu kMu}kV+~Yz} Fkb=NaԾ`GN=-iX[U@t U-)tW:NPs\k ã"|<}l=ds6GQM-AAŐpWpZ}aJ (-j|GmfWHiM#8fw o\*%kW]nbg/b%KEͿTz>`38jӦP~ fi+yX۸n4R*ޓtVGGyԗEV)^Yhv;%dqrHu@#BTkɍE57x/@w# la ៟Q^e̛EH8^{SeƵJi+ ɴ"p#aYHa)4Nz欶.@չ=w?Ye^ya׸+="tݪ_wv? n߉ cbV:{>PC9(Ӎɸ &7qb6ou!dQ{ўTPCf<6trLw Wr1 ˄p)o߉n>9&2l>*ȿwόΒBm^ G 3Qhl@c2 ɦ;̬a]'?s7Q$1NXbOi6jLp/!Eh) :'&¸)s/K qel=`%`} ( -Lg13D֨">ajխ5 dR[:@[T+{A÷dGĥHRyY❰>-ۉ<VuC­zsոd$C%Vs`>FuZZfnJfb!D^U͛ΆQbցiuhֽوl' rAl%]v伹ߩlQ:qp|K|7bJ&}X'M$LH=" i7Dj0gzeظ/ w~綄1snkAec dMjA84(*{IznF

M(G\TDC #J0V,-a<.eUj50A=۽Z7Q-+n1ΠӳS@GʎウU%)lKPMc|&ӫטE [ôn<g@Ţ 6zD#g1Z|{Jɜ~ ɘo_r=n$tctC*mL\mWFG÷؂O+4 >F ɵzuTyXv >Cr z0GrnhfEO*9fH 6eџ=63Aw5א5-'݋D26;Lٔl`4Ճ3iҌsùXG2L>.#Tlg<1Zw6{>sfYLqwp.jb XX77rVz2x{}j3nk*/>cclAe, Ǹ0Z9Ml{ԛ6f 8h;emj?l_Ӳwr*=U:5@C84\ҥsYa[|ITh;f]2֮V崃U1Xxx~?ph^FӬMW-JLP C@`fwRVDwfNfֹ!a25]/63ٽy48a>J t|S,ش+,j=Tos3LT~-dNPp| ]‰kFdEGJ 6x| Sa ?ڍlYv:9oIU|T;Re}9HEqήcU.>4c֨O5`fGO!6iFƑ,]- D>dOշN}F5FkUhnqpQVUNU=]o1Qu(&H tE3fItmi#wL]k@V׷(UiՇe H8&$2)>_hSܾaY#~"խ"IrK` [Ygq'bpW,I;#nVRT$@˓;gqࠐ;!pLdRE .捉rgCHTg۵^J>u͐hxyBjyEKGupфX?%&dz"EacDm iHx6u,Ozc>{Ҡ&8HG U3Pd%T^;N"oȤ|!zy*L5ZҠc7"u< eYXC9Ťv-8* dW=u+،}pOzWBBc~&0aY z9iߵXDbm|kL=~f:6Uicip݂Ӧ=[L$B$DLع7au εW;?Yf O7( %PMy{ Z8!I" Q4yj[cۤIE*eB)7d*C(#}e¢ܵ+DQQ/ r$薾-BJς|=˙}M Z{yKh?ʨeǣL[q+CN0 w,ꖩg.FiԉOd=m(WK.5&5A.OfSb6^68y|a!{qAuayNEhAb@Yqo/X5a.; ^Re1/50V͌R? DR$v3RihI$³ ֎8D3[ȐlW*vyȟi8M) >(P0lAd ~(w:Ftq"G W྄;xd͈U7mN/1i5Wx6x75xن &H\U zK-nsJ,O6f"#i pqWxp+4K_*?G£7>uUۏ&vɉ9vb;`!5kY)l6 P#kJ0t{דMQg X'nSVr<@8`Cy`Q9{碇m*Ib{0CVow^!ߌ[DC K@ƷiӖ<qPX|>+5¥qp$g 'M˄TV"$XRqLHx~eS_8>qYy^X A>1RDӱ3*vDfBt8Xh݀lF,EM} !پNr SX跳h83#r]mxx [NVc1"˸w)8*fhQءT prxh]G=)z Y+_iGt+Α=6vYITIXCBG`b]&"g\R{3'_%=[C l~[X-gL}8X8-Ņ [Ru@ *Gm$ۛؾ0{ɛ8}q5ɳXp'zG Q 'i (DWOaq.g<iZ`J X[znM!9ǑzfFN m!{GTm^8ZvųBLɻҰnqG++j/:+ku'ƎXfAHKSIc`9gO^3:z7Qztx꜐y=caœ+bF޸f_lu/jD݃fKjtܣph|]ZH}A]FA; !eJ1AQ>J$=AQM|r,$8?f‹w1<͉ow5He SEiv.o_n(-氅 )6NKY|"wQ:\4C)/I!! lwUWg\5oe&âN}þLeƻ7d|,P2S>v%@geZnzv^X`%I sݦ;OL'd';6b;P~ `P3)ğYT?(d{h(dTwѯ[[fsgD4Dž]'7-,y|ْLGf!PriO Љ#Z'' Vcŝrw㹓>-#T0p/]eږ찖*"\| fЛA:U,YPiy8q+!lp7{&iz5N̉pE5TfH ' RDն&&CZЈwZ-߾{jR~Y-4!6,;?9aldbٸ8$j354A%23df/tMQ$b: tӎfbM oIHl(`y,-wMZsc&VZXwRd6 L%funeQ3UePF%b.<6P-4<#菐u+Emy:%(&\Js_6 //zH.vD*N=wGuF#0q['2八e Ȍ;>H8$G#"nIhgȨGA%ft۩ Y8Mjhj+oВ-zԆhyH3qۅH*_`YB!B3|sW!KJT=:'}>JfUGG q~_q[j&84lVt2 d_u n~Gia&*@ !; %؂aͦ2ZXn p4`=lIgE4LDv~=,Q8 ޗn_g5({fx^~}KN-%3 I)F~ ϴLdrGG^Γ='/R*IXPwȩc%G4 gfQ=I3 R_dtXfٚ`Y!EkGL̇*ɡ_9k/ βo#],WX1a6<- 4 -@%X"Y@=.%,u᩷j J=&K=gb Cw`90Fmj? tB N ma )otZ_򻱜+a<wqΌuS0A)[5ZC V~j ȳ-;kmc"R8'L¿wg_'YvVPwDW714g}2L Jc ]GkE@ɱo{_:ekMmܨݢ6Zg3U>QV6 #UQ= q>Vɔ=úb3s|-J>I]Av٫nL4‹,`RenYqړ3S1jhV6#H.pR'$Ҹ)(02OI4 ،4h]N7^D&eXz7x Ou9S ϼ d0=3s}(i'm39$gn~^Cƪ΃nq.9z!<ZyZ|ssUA!;W" Q^;Xl裃r CՍ,emdy)^.0.`L㕽v[ N ˩N} \"8(CfqG*쑶}^lHA*; 7W V]Z >ײ|<{XP(86|YDeIjѻKP/@ztP['b-ÁBcEx%C)qzȻf!&n5&¦`|Nb Z|}xCnDjYh =% rJص[pcvJC驼AgW@Bzu7~ٮ',pNs-B,ngL% =ǚca-s'hX$7"M]tNɷ>/h똉feP^<#0tnVM6 לIupc6P_O{zu*eo\L+WW9'N]gm񕥳\<}T7.}xCf ;vr5Nv{* i_W|M:jʠVd`5&vi}GPTޟz/Om~i^:yhG|/&;]ě8gV3xF {庀RIx]ixr' e6NE&c:z?i϶ Z j筄#v7?tF 7L:!euԁI6GA7* q/TVpPJzC*-NZߜ r,F$&I<:f_$H4i+Ol (9@> n1:$؂GCYt}Y T#h.pQӠ|!7\亷Jw<ߺ,+}7vSԄ -uNak2@^]K}fh{Yv#R5rb^J1k:̕1 G5!b~صأf :c_\^|fV6~ Norkq^!Xm.LK=20#N)b$]fp $,_{7;hfȺCY[c?i+Wb*w 3" $rv'E+p*eh !XdA,N ^ 3!],XKSgM=3o̠ \Ξ@|ZmNpj9& HHx 0+y*uE~a7E]TN YLFjCE{fO81$b/>BD׋i tӓ^MT~y`*+$@LcgYL܇c|c]K r_b-jO#o.%sר K m㕒Þ r#Y2FDε-J@Fpu'RB3@ odhFM{kĜVj^"2Jp ]:o/G{?MѴ q0` cpsϩ)t[G!g WnS"qNP fGzkxibOK°=!۾G Y.6M'i#GGu0kK5/ej2jnAOMP|cυ0!t ai+;g)m0ՏT{m=sP+dvCxGS 4,S,Cu@bBCfl"NV7RUөЛ }T nm)9kmt}2;me1'e>CSR62܄K妞U([1sPC#\@0>Pݰ| EiV(I9 Uc jd V #`$vzy؞e#֢a~60( -=X;LڦݛW)Iw3FBetDMB_s"W&F,ۭi 458TS$JOLƞ0'3e6Iw\!wců7ݳr~١,J,*w )ȶr&SZ|#ҝEG`f釿tK&URТ1|eO&FPam"MƌuQg׌0Vq!{A\?j͏z!DkFCG!w/(ɂ14B b ?ftb__Bn:ܶikқn-vtTlI|:u>Gw9U[@z61݂1q|.0o/zo&!d& Ԙ lҘ/u `U\jB(m Uî u#X8[\f Z( Qo-svr^^tJ-cRx?Ɋ]r"/!!n)pF }7Nڏ48jfV|9uUK~fTpN\; ߩ)reMԏЩpYHrddf}߈.Hn:pO<նFwts!A1 Ԅ FqLLGÒWAwP$_U?L_ ںlԈTiYhLk2tC+A9(̬͡:0-j.O[TIk{GNڿPMu+R\"ca+]Ss>Us~J)A*NOrCIR{/Q]n64NpFhv@f{P9e絃sIC~1KRΠ? G/juOxa>-%(Oˀ/ELPˬCYe;Jҭ : Jvʟ2")<$XE!d;ӤC']/(.Өx2G)h"JlY;Q_hz @& tme&I喃?;B(%tt32(41zafuV$q1xl1c`IJF,p$q5?Ǯ=`Ϊ=pFa666%۴T&Y5$КR`R\b~a M%ȒX@OAy};tU$?X.ęfTK rQIX` eT+yf4rK _ cn\|A3s^\k[t->>V-K-[GEYc,/ |*%d\ϻx/:(O[;x64 Xѱ)EQR7ℱe-Va AӮH>Ϟ@YybF7bS=Uv 15WT[z^3P/?j56o _a5VEWa+ Ekr\pR_; TUڒb0CVm!D'ktQYS(&wIh]np3 צc+ +,v{}!6;h`46"c]sq3RU$%m. mY.Y-G5\'˽sҨH4`U6mΫ9>|cqQP=j.(Z8ow9O޸V9gs(mE?{vU5H 0zy5`0MSo_埆e&q=YBS_?fxMػWci]/SYGae]p-JSpie.%X MMw(C4zy=)8תtbQ sgMz9PRdgK;Nmfw0TꑁUq}L&1%N`І\@AwfOCx[&&^|"i zbt{̀ $Tluf1Q.ʃ)?זJ_mM_l:a,r}'LZRƫ'ԧ֓InFG&/rf$F?X?CTCћnh,fttpP64=4=k!Ї`>f.}*ೡ |q)PYt·jNK3Tie*A顶WRH71!:dg@aGKHXζIa߅9L>fc5K}q<4- xj"a2:XoC) 0dNJn*.Ʒ~RIM!P#S2Ote6zu+vJ]gv%yZ2Lƚ$uޭ;|[r~+;fϥs5%HH\ߞ.i+S>0k9=EX,~2dy (sa 1 ߸+]|좵.sӐVdv0 TƦԙ+u t ܻMJ &fiO,W징޶<9,ax’O+k" @iΉ4/,h@l1f5Yeb{~U0= ~PrY@׊FW* T`Lӛqq76G{w̗Hu "X5HF֑)m2IVʌ#zamd/@5͞G^ yx$i%8~Y7F0{FoǘF*\^)NkC*!4 <,u0dGhb&p5&A/rXӀkXū]t{)t;PP#Yn;~8\zG#HㅁIDjj2\syP'e`:;A RҲOd^8+qxha0T=/'4 e ѫ[R| פlGftAс8;X-X Q]|ҵ*|V(m!J r4vRb]3֒4۟a]kmޒE"JK|%> (Dyfz~܄]~_x'G&vހ-8q[V4oHDf&T:.vP1oL ^L>-iNdbrfHV41hlL\~#}w>(Y%U%!O.,;wZ#ky UrB0^B|1\p?˄=ɼo;UDZ{+AmR`]^Z=Jӟ~#h&Fb6$>9Mmpr v+V5Un3f}v'#@# trZˆsϐpc7)ߗw%az\RиhM`n w!  ,EezU>1%_fZO{ ^W,S~LfFaN5齩 Qz;GP٪|\c<{8(/o` t_dYn>oܰ5sk>|#P`4iό<}93t[H9ÑL ÖɾvZa 2|'#" .1{Mmc+Y.jK4sM#:u.ﯖJx93ʏ7&Xyy>+"&2:_`Ȕt e, rޤD-ZC6~%гO!QAftr"vLŔ]fFް7R#Y%'mR5jN+rn3u./9 ݋Zp%HhYMy\0PǪĊk'"kؕٱ/̃X8]ihE^XkVcY^dbh=_zTs7Z&k50zȮMFbRL?t߿l]g?I|u5λn1rkx;t]XG;U]*MOמPʵPvi/6Hh\>\M/i> qն7f* c9Qxs$ ao+{θF$'sjTWr)kDSa'H H۞pz?MJ18C?Bi6`l\s']sh/MDX1MC&v9R]Ё{N׷-RT9(vou[PY& UTR)Gjy/ThFhJP)o XM`DȜ{PwFMH}tFUoיn]0+wǗG&7!`{+f|csZ$[١{#}2L|dž'wO&GыӬap7ƁK__NMxQG>{ķ)^jϣݍyj0:c2]8j݇2$YI8yV[G$9;![^_WN95:JjoKT6H5 %=hf*X7՛xdׄow2"yG/V{=ӈ9h@ s5geE2$ߏPxm֚wbe{ܫv'p6-ڰԻB2'kZos1{ : Ct%h\Á|) Ӊ?pi!/ͳ׏SGîSRZsxccaݻv=@TtݦkT;wqfuwDk̅/ok'V!r%&d@IKkän(a9 !05넞O*e#",icvQ=+ ,IA ]90'2w- d,q.Qo޼Do uWm_+A{wIs:д \Y\@QfhZ9ԟ%Ss.:yҤJ[ʓE,[2q67Ӌ/TeC$$y>ԌO *95=yԆ*Y *mmQ/P{uQbPhj咖ȡTن\_ŋ鶉 @܉o7.,0m;qrk8l!`͒ӳ߸ٖWrI. ?/H`]s MJOcܢ"^7XdB-#{ΌIҍM$fk7z}O郟Gd WZ!4:^qJ_fٙnl1_ݖ/-FJ$IYaڗ mlEЩbv !||jxf٫r 0zFp.`c8u+T f$޾3`j>0K#}S0N@ * P7*_('d .{ NpVi[LTz,qFǁZ- R~MIj*+̶4+tlW3G{,ʷTWSWȟ%ׯ99uinUJ.pB:7(5ĻwBJb$>t!*9%X 8GU,dv!QH-$7ݲ(Kʲ~;5LF7zK1J!?"sMCE4PCJWu4):. )HӘĪ pfǨJ91.=64L 9* ^lULfռM>*%[RT/iC޹jtʛkWロh MKb30O'pˆ9<3go*9hq||6UZ7yQʇ: y1T"iܝF`6mYJKkף-Վ%Fȏi?=u, /iv6 H9ICs#JD]tu,nhaUF4ƣwB ZSL\[Uޢ3̚~=:Lٹ5QKgt%:2_)k;(g̦dQdz4;SqfP^A*>J6 ϱi VaY7'ai'Q:ܟDŵU\WX=3.:QpY] o-lGz'܄ח4V6og1\P9~ǢI4'C^DX΋3#Y*[7ie[< 9.)}M ͪn_gĄ1?2swܖߏ>5M e *?қ3 R)9au 5YxD l1Mj]'#QA`o>>TIl"v䢋#V6AV.;72\&F g d|7S`!o-hKXy SBHjR뵃q&Y\j-fk, `1γD6 d]cSy4ܕE::-kZҝNPߗ}\'=66mNFRU's>,xz/C; /#s%%?=$2:KJ  f j.X~ĈcEq}ONJT';3I0wˇ-I4a2MCct=ia'4$Ei!+g۲mFQݺ= cA*<2,c  H3#lGҨCqEa&j70{e `'r Gi ~Z 4)G±Or2<(oVŕOR!M)j1}Ts^$l/T<'f>,H3`-LXnjsHPxei*7E@è2Y XjS֯iyϝ>#G伐KgoͲx}glhy+yS#;ِQ c~n zgkąGO~:l&{kx2WUۮKp9T楲Qe,ET|MjPܢޜLMk"'h %J`8@d d3, !7 Fÿ[+:_~w]L2k4c-ۙt 6MvX$Zlyuv]È4E(!"@.fQc܇;W?+RYcoA`Ynzfh(x[]]z'4։w D/ Q{Q{ Q$H;QpdZX)(w5uVLd~uNAxZÒj']oC|+ L_tF!e< 1??[5%jD]UZd7// )U8 O?[L^;G=LpF$\JB&ߛ%x zZl fqæ"> 1ԇcᲱfjT1qQ &l|ZȀ髩a/mdB (J;ibU,e@8.%>p?t])`&(~$nyݠxA_WX2u "'+gKhĘ >,wPB}D8Q 0x]ȅȑ#::I)V=kV :b (븯1tk^8?VL,%^B\טrqg2Ic[Ÿ\8R{/šKQ2lq 2tgt;*>46:Pr%*ҙr۞m"s@_ԼߘI7]>׌D.]#'UFtMmAL$z@7suK4G'1b}xy?VS_PYHpgۑ yDZc82^R*Yn:x*Tx$|rޫKD`1rK9.׬r5fNDd1Mܢ44Ѯ OD,j.Q(m"Gp5= fHI-f6HUA_?Kajj=z2BaQVcC" BT9vzC0i -c}Nd4X#y_Vt>,jH'-T=s s|sxl]:nөۈxLz?ALhj_^v3ko ciЈaAʾ=)v (rGakj 0Nv׿cC{j;E段%zc>4w$M–4ܛ,IpKL4Yi+Lsuh7e tuCVS\gDޮxR u"Q pv@ % bm& 0c,zLz][^h+zu~er)R͉*{W@4G$u`-DjS'$g`0UtJgiq!Il B -iX v`+4Kˋ\-=>kOK|1&nϣ)!U,3TWJ]0quoqg߸&)n IdPC~ύ/S 8o~iVCZoͱjY@nQ]Ŏ!C5;@ЅZ$P_Wϝ UyqN^I<劙q#)r7Ce۶ftU~s)//bh3V@|t#&x–_ nU"+$/Fn.@o!UA(<Wy  O27S>Hi jM^m[Ga}>DVrm4ۣ8\=-$#Μ-5tVdak}Y_RyZW 98sO1cۚ(WyIz]AJ NLB`s0<ӶM5Z\!M+72:-|*"͵uXQQs)y6*LMqRb.Wh-ݛ1,d\ gŸhz` )vacZF?IOUy ?~+uxbb, g/gKc8nĂ+[ 0snhb'ݔrRV&J!{[_.p-%x'K*D7Rͦ_iT¥\߫#Pq+d/= ZS {ٗR(e*GqXO`>,e{n='r%*3`,V 6Soy:\7~ͺ ʜt\VNJMx Ф*GC3bOnXy1Z WÌuH}9Z淤vX]Q4 l04L iuS:շ K5P#qIB/=XIW d'B 4Zgկ(薈Ef탘 h$ߓ{Uoqy} 5`r9MLPfBo1P،B! Ze#'ѡ"-HrWV~XlnR:Z=5|J(7*|L,rr^熜[ t׎D'hnn/vFAEը$C.Ү#vtFLl|t͞FF7!'?ޢ?V90C^ū ܍5;BRӚÙGz<6k(䔢cO+[C{9//+[pޖ.V焈 R G4TR|=%Lt$CBmqfK|t]Qa=T {I\-1xf_3Q\^谅xt#.?Z}6+d>`* [ 31y@bq-ͽk{S)M =)8QynasB8CO"%!EI 5o=t]yV v,SoW&o!aen&?Z̎Yʭ_V3'F\ cEZ.W̳ԍi>A]tpCƀUGҲ$p2XBgDe\P;JgJ |`ԧ٣䳊{F4* s(mjdN2vMif:e,~f^%ϯ܍=D] ,HU2vR?&~R'Ӵ1]GAߞN/GpN #-"1At(i}|5hdre8۫4ŠɌt]~" I-;~*{mMͮ]9.!Xbw`PyNɩZi8lGYǭ5Sq>7)ݣƠ d#wsݠOTĊDr3 bkt̠GDȨHg6ն׶Pڡy\MmpT dW&yˀв ~uj9.(DX%=ȵ>kI&VeG B4wU/K -d[)x/^4ѰEA>Ee]r7#q Mh)?aγ_w7|\xr*WWl.Ò A2i%-?S:2QK[}Y^QfEw(G 3ॅjO}`աEcP̥SGCх{F4Uw&}W]g_$?G6_g01+++ urƳd{/KV[E[_}lj_OxjqNӛYש&/sQ(x pZ3ACCW[6ƴѐĩ!}ٜ|ɼt:qZG1>*{x9z̫`j8Ә.h#jZ`oj砀/)6'JDžy+ 3ƾgW'1FF:۴~ ._M!<$PLEqP$Mglc[pn(m{y{ \CՠOg?s\׳MYV8/Oxa3QY'y>]^+LE%:cc*{Hs(D5Cq!ێtQl>rP_cjVbZ=j~>XzպAqĉ} H*n/lN޲TLp_ #rt #HQ^kr $)C7eSiiZ,Gf{Q*} =H-nW#':b1DeC~`?$q5shI(?j -|lO?̗ 'n ύ8p /A>}!s]+=){1̄u"(LM^;oqu"EFj<5^ePH1#TL,+sLH^`feӒ xϷ:18ilsA& o SZ^כդ6)P̟Y#_=nڔ6]M!Pp崵WLeE:\S:tjYx|n<̨*ŲT]qxxY Hj NfXQM1(g"MOTIzcs- h5s1&eT2[$S8|фkH;ME`C"-=-z=4mϛV~2X«Q@vw1 YR!AQ1A9[Eq g2)nK1HUD,zCFvQFeZTf*\Ax >\{ ؕFnl 3HT0># a(e!b4Kr!1lo"V o(El cnuV&[Dٔ $Nbyp:( u;qLx'a&X3E 2 0e|De T3;lNÝ/x$>U7!shEs 93`G|_HskЍL SHlPֶƥъ913 ְClV,sE,k'e1̺c 4Hvy|및ՎO Zٔ5ZzFDQ,~ᐡa?voCZCmt^ *fCGEoj/ Y橦@vâE[yl"F(ҵR# TN&Gu3'krWfXist^ƸW"lael wG}=G;AJɁg\Z HaƷpoٖd`S4j\`JTfg=lMxP>Pt)[EP @@L"%%QRw j)ϻA G@Wdƕ!ݎ-r=+r^֔Gg3/ j 4[d|vǐldnSEs(֮51ro)~G/OG0u>['֗/8Bl4I-j1 u[' [ܶ/X@ uC/ `{r%拎z ]孑qn%,&,h0)E}3zh.+$I9POH,:Ba4اMtfu=ڎݭQڿxA%Xyp:ٿzT2Lѓ#kIsuRi햢=qj`5"f}@j@gƖ0XOYbEo_`n/-~ƋMmπͻ\ěcSvQQ9jRp sTA?V2 )+Rl|0!Owo?'62h yJ7lXQp`Ls݌i[½x#rЗ=3B̺bn܋m5d9(] ?ލnڑ\Z?"g/tRѲBm7[) ҀaGi;SDl|ZmT] (@tDIxvO\ ͂A'0O;6tf(-Og,F3{d>.szyUf~2|b# :*Mh M8[[nU}!706r'a2ˑeQA<" ѭ޵+}Zaq/!ض|f%I1G?>^BB>5Mz,C@랅r ∽S8gۊR"*6/MR8X+ݩ2SJB+ {NC:,fN$go/yX2B~-P=P HI/=SD./QcF983xjUXY||G_FkTPjJ隴`mcA {, AgeS<} 09SO_V/a u䣱(XڠF5`ʤlI|B%iD~w>0l_v~Ř*ST4;t$<?J,̢ 2τ Iƪ >tǑbMdFjaH { s>@.OfPc&`,QWt1($(TX,qZ b\$ iD:o ؆!g*  )Kih#1-G؆KΆaMQH!b;l/N4m?a03hoj>=*ڵ;;yHGtƅE#+)Y"Sd1q  9 &uЅ^0D-`.ʠL0"^yޠiDMy۩JrD@f#^__N}S To멏s] d@/~AY_:O#qRBT/O"Yny{֚ԙÝUn.   w݃>q2ӌ̯5-]T#'7çd Paϲ':Q}]0|5ΌOVؒ%aJ"s,2Z,CŔگB'=J{uXzD #&A~`})ɄEm}r}M$%lr^9BI!fCe?ن\(2/$ԣg̋q¨s-.ԘigKHG̻϶j.cna50^RE>DY$ۓUd: U;Xue0,$T",6uCo%>1 DB.ǣ؝Y-8yu!^u;Sٳݴ&ejo| !?aS9LmuFlp}dyeϙ0_qph 4T K% 0#̖Ͼ]=_~3"H[#iӤ8jԌqӠ)AL9yx1BI܆ȅU0lϥ'Cg 9&ƵwǓ'-F1@!0pTwWlRbtB_Fsq=!r.p*LiTjUG:F]}]  G;Fבt+`0(WW?@vYwisAAEp]v"XE0d5{m QuGČ&d*"@`TDŽ+I9nr(OqTtNC*6BX`HO"p^NGZzv3~ڠLבx[O#Xpq'9UL'U4=.· WEIf~0 [P+'#ǵcS[A2m lq/1 OO1\\2cMR%fҋ꣚FZ}uJC=?Rx*h0. ʼng9<˧‰:ӱdې;i rs=@KwZln-yMy3;Wrk͝˯ v*4b`; ²>9ے[4Ї!θM*=zp1h$0Uxi/?s9ǼmF8]~/"#Qk`o5fV?ɷA"YjB>6odM`BM+|YGo97@+ -}ڹ&@ Zå6m0P< .4i75 LvE=g؈.9#Ǡ<(wAGjV^Y Esh*Ĝ,AD2Óie:dvYˉofRꩅc "_+,^srּ ;qLwb"HZCXJV). 4*S7,n>$.ejkFQOqi.Oן jck*R`5]g\@@ZbD&qw *MlV1Y3 l5*ua-Aat30œvj<:\O~!P&褳UR f}'Q]W-F|Nd1W&UwA@̎tf!ׯf?sMI%tmۯhxrs ;!$(tx˞r[" "y05SC}LP u\]l,<d J,Q7ꕐ0c`#OYlg?Ȃ H-fkwB=vnA{(pMɃ VWS _0kքÒ  k^֢$P,fMGO]BuÀУne75!nQ/b|^x(4{w; N1!+N {~ҽ=beN|C!HIS 1_,ȱ()G崛HH?e y,@滹f^ъɼ2dU)2 5*&obJJ xAE\JͭzLu3PhD,m )V!#wNnӀh"qf~5>: Q٦wt TV]'Z%(R  #L2DGbOt>58 "MӰ߫b Ն?&{1uRifي謍^q+U'IYĞO~6izZĖjOoOxn埁z@AfNcE,v:6˻q=˒S~u_jV@ oj(6cr>o2@Qfr0ݴCHtf!Yͨnz@cQ\jUׅƚ(6QF) =c V3L"d-8KY^B>LjXcKZvYTSw@Xۻw36^4-mM9=*?AK-wB>`A/vTrLPoۮIb_s? '.yh3?hsɡh*? ty$o/q:A Nuf;|g} r6=B$,L?:^0r5c^h >\l0^-B*T"T27K>K!8샢y eRrQc@x _RX>s\q4g v|lK| z:H [=j_j%ʼnHAG 8I/\[z) v窔bW{Jo{6qi=ZZ}XǢ+_G7 #4.#rAb;I$=<$jR(["BkCp)L# Ⱦ[7㔮#N^zRƋ7KI3>e][2߷C;tdl/7(AqѤav۳NZ Z:|&m6m;%LzB$y0ưfx\_',㎍J'P/a &n L(KCSXrB~e$㭲n c0r=E D/QZ)7*ZIY'լ]9_cRtYIm R,"v>m;2(x +H~,]&',L ej-@?k[0`i|E|Ts= F*&DD4h.Rvw bX^ }ZDazWS[.k6@@;64N5U@ϵU bnBCZ"k&γ:qMmDR*kp#=o,UG8 h7m&ȃS%ʩ2cIK6+5;@No;۞a%ފ̓M%sl/L?g٘@HH4J=U1ohKTE'|=KZUR,; N;v2% ?!4n ;WFI1l<}r 6/yw=4ow^dZ':DJ叁/~f _1Җ_=QBK U Mʳ~DNӘr1OBSn|bHߣnNmRċϾȓ:=w-e C`z)!O]eJhpgu*`bQDEz"с _u`h4Ro;:T#D 5stS THos nEXg;v#kR(M!~ܣw~vF$^'K'&+1Az8FOW2 xC8%H2_r&D4{ ͐.wqS C``JZiM4e5ytuv7H(\QƫP@HH ֖u!q"Nt+z 19PB{ uj۝+n| g"'y=G-sglg*܌aRaXj qK,CZ΢]^L Oaq's 1EkWlb9 mj2t"𯎸W";9Cm6g:VʈCS$u/K&!|IiQI_=8w<Sa- ~V6CncF"ka4|ËÛ<)VGd:eV{R:_B=ΰG୹\>1>*p34i޷ 9>l)`۸TK1=T~Uo̶p tW2O%^&)}3 A -U4dEּG+TkukS,ߛ\ҍxKQ;,o?_V|AޞwPa?p06 0z?ypW$"kΌ5A?QEy4 vTX*?Um!(L~ 2g"\7:L*D\\W$1f$vڵ!H<4t<7! XV11cgNI ve|zS Z/6e:HfȲ\82̰X2K(g`f0ISOJ4vHeP%ta{p5?&a1uHu(rS^yaJ׼틖 Nꙗ#x}.e:}gW5 CkdMJn-ڄ_,lr߫'fPe6^Z%})blYE2]nhʼnqr=wA'%23B7`(dRF ^+zPyhuS|NpR6-AphO_n]J"7RG.P/R~ %ns:YM$N؟@b_Vɶh>au9 ]g 4Nr2O1M,8}ST~$3ÕŻ8 ?Op RMσz_(WSPq/ҾL\p:3.@(犁Q#A&b{KJ:s""u8a4a٢\Os*dvY|P1O.^Ң$6Q/A"P!X[@+OR[ֹ>$>sKA#[uRM7#FLh D<M!:Pf5RI afF K# G[;V8C2:_F` y:,֟2)}OɁk 3bYd(]2Ɂ@Ca]gD{ncN!;~-D$pbdJ"Po%Pen.j mQI415؇^BoqTb}Mo{ *@sݥ;5SA2,yH}[tyGݘb6*&~LW= *5gf#*:F=:} :p&% oX%yDΠU_9D-*f#O_wPwkLǎqSKʆ3hWPZH2DT4]?̃ߛ fg|9|2,Kx,r{6[®_2 PD>'@ uT)G-:@RHf/IcjEBU%ۙd WoѸE\BmE ݡ ;QF?7S[x3C 7Ex\ Kɏt~Fۘixo̥cb)%<у$yv(6=1mZ8<' 2+Y$M_̲ZU?خ<l^栦^+#&=`Fɟψ ґwdou *O CF÷.e)ՍL`mev/&ZhD:QOm8kSu)~6L ˉf^? j4mH;hwȐbZd,q? \.S40^FeQ!-QlvެpIx`!ܴ]}KUkFi;8z'B +<SSK(vR~ozfOZ`/|iexG{ǿ~Lڞ[H%GM*a'^Pe]I  ˥"` .k(#Tu7f&-GׄB )-bwM /37EzJ[1f?ɬ\CJ{us$RڊA'*_pRQ%ݭ0= U-J fiօZ@cJ?Y|YM_i}|+b:ZN]gxfOY6A~°@eɿq0ye2dIjH5섀zFVJ(IRh@6ݛe,.wVh.G‰Q >b=d.nв6_{yAzP8Rv!os6?S%JIZ܂U,H9CQ "ŕW%M`qjZ-eU?O!4A>V]r)7QC#(×|NF՚ԗBpBRû]!WDAWJoP \gUأBL / `cJ-᳣я)q1ijL0t%fQ= =_߷7"xsQ`ro6!7j.Xŋ Q'EQH:  p;NK90]s\ݿR'5~L[~5hxA>{cYTR|"jNt[&!-=q ֿΈ%cE:/pfɽP\W[V_ḫ /dڟ;M 9%Q3eXN(XPvUX}WR?%ҞVF҆6~ o\)Lc4K[Q珽abkI#TMyrfz F, D_\z[8JQylCۮO%kh{!-֦ 'U(j3Lښ«5 A6^xdfI$_4bRqڃ,yl`LA#c3)^U3L eڷ7Op"$IGwޖH-߇b+k~&CXCh£jDmR .EǼb -b=t)X(v$!;@RtE lwB1Ebnnbx0|ш)۫A=ҽbф+NI' }s,5.jf5Y@yȨJJ,śu_asQϡϾ1F _?gniОɵ{b8Fsϓ:|=r ؽd9CŽl$03ν\E .SjM($%EPPmCZ'+}> N@hyZ4pzJ~?7a2̩VCrUo/D@3kZZ{z\޿>Zc gS6pW|`(JѬ6piQ|ԛdPV5(4Ÿ33?Y{k1ñ0!r0[$>eɾ31YrEpi9 'nYoY\eͩ{wsUKN)4WJIz~f2I-r6x69-  +AHʲ% JThr«ͯžCh'EP-uچ5`GeŦ4-WUCma w{ҵ_QE[_UOՇBi/]3n3$ZUô6\J%v` & </Z&y HH\ f4N#nCGQG}>uR +r\9BZLޜa+7&O,l([%Ŕ0f )d T3ST&S60Qc4$έRsi<NN+€-A{džήHBj+z?j\a%x8]L(Y Y}'u|}965 ,YB]qSYG)1}5C%xx`\@v12,/$sk'㻈a$mN&pSɊ;[*!̈́i8H-+U//$:daH6!§f;U3yJJw*\Ƿ٘2j>O}vrmZg7YKSH&ZyސX=3qߛsb^JcMucӘ|{r^+3cJ/$-ı_ol8w%ܦodZo!XVN!CҰ.sil / gtjskX W(J8.g@#" b@d-RUX绹W"c 5r2Bp XCؙ;H;*6/ 3Jz<-Pe|z^ V_4Lrb&xzYذ= Qo]n+XDK䅮RjBLkqQ k]G~q'JQ>ڱwsZBORT2whϛS 5 Zq5So>ԟX*}⾲7F~ T(!{+ȓԋ_jU}ʬje @iVEq# u7t )5>e}Yn680I*h9)˥[k~`Å)mZ -OPC\ʅ#j[p4D53[ߠ|;Ru?\9n!bٳȗfN[Ɉr 㩯8gC-&,⊁;aGYjf u'P -86fHiM%5(ֱP"Hwa.]8#\!]9ѓҡҾU!Ѱd5x̮`EhֆtìamFG%ͱ<~8/+Or0$qDC^'БxTQSU45)ÒajUZ#=2eQ9B [ Qt\dv>l[^tTşZ]ss=, p:]X,ZL C:gE[-8G: .nTi$>>3n^iBq%|=|3HuZXvG#y<7gI}_Ew-XٱJ:BMkc nX+)J} ӬC0/;TӦKmXaK?%YQ׭d`d4AoꃅNĪqּ4|Td\c㒳"=h͖?^$YF1vFo{*[F+l"67 Oqe#5]E)^SU[R g^JPi*1_I|Pø.:^g O297-'͕UŮn4q q/:/U L5`'.7ݕ(!qXUÏˀ`r_sҩi~${ j.0W++"l:>'Id:k7*=o,h)<BMffƗ,i@hYKLkd36YjߝAzޡ&'(T(AooIΈbcOEz퉃BOөOei?co&0p$'3'Q-! _-B3(oET U=^ Ov#i )z3_2\wJw- ViQY|6*-%o$vm.3"k+L АO4 e ;s_A(blo|++gPQVJ!t0)mƵц>4A#\Y;LRJ)T8X>;rSgs5ZNh"<7R˙ǔT^[!ݾ,B-{A~-]M~iRÉp<k5Ҟ8F[ri +ޔ8lC+y?0}ɼyp&p<:`cvܓc=FC }ӴO߂am~=n!40k1q/Q ;qh H&c!N p&NJo7~hQ&ysG.)~#6 ZѕwD:'X-Am˩!wmY@'l4=tOhT@oɭS& UXbbb7Xo?_g)WBrGH J g$=c4Ȋ2K/]7?1b>(a(Šu߁݋!fP6)  w[hmr"9P@TPf>mTl;dGX(: ` b9xXBhfdjc=iH5 Y zQCX黱Ɩt)9a]w׼[7*+q. 37A ZiZ@[aɡvyWޛB9U3dDRg`㙹R?^ziw6Xiſg"~r;G _%և#\oO)}D2Nպ08OAW ș<$ ]:—uoZ[wN\'1 LcwmJ]9"Nޝƾ W>%[. cߒDŢD_k皼] 4u}<Ҥ iJ]CGX@G]슌众qhI.ImƩ G#j@>Y'P@e:5VOp3{ךEFXW5GYߨ%0:Ԛ~w@ՙ+fr?>XL@EGEN9%UfL\ b&V @?=OηU4W!^%ӤI۩(?"dLӕЫQʆ3fls vO/Ј D^vFlj0.TfIyVNDat;`:Wt8>{kF+,G8x3 +-߸Dۮp[vE#u~14SA78_EV J (Zjb*J䳝-'+ޭ0ek=H;7 dT-ۏ܂<{KҒ(3?N=q+ݩs1&Qa/{߂'rE[Cآ*Cn&A;k(Q)^ٝBZk‹\Ga\VmDEmӹ_`| yX 3'iqX8 %bFJkr Jd6cpʛtk<˓I.zLlBmKHip 2#K7fL95DBF*y]|阚 ;&0 Fp$ D^ jGyu"tȂx/fYX/5a'ƋF'uŌ ^s[A^9~ fk34lځjӮ)sHEYPD2WyUGEtlBF f$r-4 _%T2 u"<*Dn3|?JnRh gaV G3g4鬗I1DML(J!ffYORVZo6Ko!KiS:8XjbLa <+@,hn0!>g6Bc;?v Vl֥ DveR crwfnOX%KNbp U޴>&D U7o1('5q $b&7C4YAQsã7^ 6D7aWF=0_$²Kb 3@rnTw=`+MEvy)⨑.mAj1Q o4Ҁ_qUݴx;Y41GfDżtMƛ(tˏ/o^Onoן !8́O?\oh>ۨJ++&9VyyPr_ {^\g+*u։O1!E|^7A~zh(ކ6՞ʽG#Luoߨ(&0gGHFqL'_Hl𞊾Y0!?JkY}"Y?"cޠ:Y]w6Qfr1JCL^J٣Qf3 j5L!1x$8FNc;U_-T1 ^)Sz'#4la1BH 5$%E [20w(>7[C7tlRlrre'{(Nu&(6/R#hr WZU3PcB=n`J86 qx#yJ˃"O,`a6) P4, |<㭵uQ;M(q\v. 7|Uz+38 u; rPBmmt*Do)y1ᕨϯhXo ;JK`/5\@, %y-fRw0YGR֤k `h.h휉G0lv@8bFgC"Ws^fy]uE3,4\?'.tWK1YƼ`Lca4y Կ"3)HX^ q{`oєS7)Gb2T[]` 9;t~uVxMa1iL灺}=d~P?"wdNb ) RkF J{` XXxz R"+9Eܔ~dTq#Ef۪>tf"qZ3'E}FZk |lb3~Olq 8zGI)7&KV`NCx)7GFx= hM? Ŷ1$Ee.4:kzU@y'tԔTeYWJdCZt5aB#ZV{0 .*Ide2÷Й7j IZJP9_klijtk/vM″b[iq4!裂tTMGΧnWE')7N\` o{-XǍaXx`_ށR bό \ꦍ:rINe*P(բSͳÁ=FT7+Yr4QHeRvٷ/[)hpЄ(kn\P@74[mpDk{mE-|" .HӖn :f}TC d?e2K5ZCEEی h}Bm_-nSq|LR#(^FbruC cF(/j衸5f0T́;ji@NMvR])4uWr\oۖ$O74'__Tx{NļlCHJąkHMT9dCƼ[mEFeR]3&}祕4oChJY>Rٯ8dE~LSKJZMJ| unʥcŸ^ r0Y!n;-7$ ,-E%bDR1w-W։;<("W!c/;2"/Tux(:zCR,w/s]MWq )salh98VeOLZV>aOIR5'.'i{2jjr:: @I*jN'e i]\ r.^|>qKek(W{R]δ}i$=|g¤ of*=OxrIE(mĚTDlk$8QmwݝgQl?sC=X+{bk.&yi ex g>g}P]j,AYXgáQtlVoQkQ3*"`NvT\YsF%7+%b`F^9WpQ8j2HfSJg]@ՄxS_y1R!r_B"5f꜊?&H[ :08b;9&dCϢ}BlbA

Z,C$:}>'ɚIM,bZ6C %GZʀhBdIJ0d4hCPTqe͇Jl\lq ( t94 Sh~Yُ߃ÝJoV,55(S@/՟\ORI,)Hqz.dhn|dD͝ N}3i5ĄBj:"C팤~R_OK-8"91Hi7BI].V6lElt;?% B% NH_ hM42*skbMI22Sm;]I]<$i.aʹs劭@tGM3ML1K2MGCHCRY {2*0@n-F ԡf7k広7{)I}e!c20 \ʴH+̜Rh-lF"҉Y^g٤vL- !hoh澣]ibg:ؖ+x6e#NN26d*80j 1y!N9l;9^TCU>aؘ~iUs}`;#߁:]n(^bLcrä8l6uOΓ'R~;hC50D*l|6/:GӉ5<ͻ.YX!Xm ƔZx\q3հ8&VO!8Ìbq7DUbXrMr` |) {>X;nָg3{=Rv"|EF7.͍rߦ*AELAil+@hvmcdXte?MIV.(7y4LƻAXdw:3<@JMx X"ojW\P_ BSgl퓰U zJ?K+#f\UE* k⚚(ዅ>6itK49RIzG3|9nxaYKt RqCZТyjҮTrFmOn$3;knڇ]q#خ&q𔩎kꨪ/=欺%VB%']+lzMS IpjhODGs/ޖh{FmЎY mQ]-HbC2!>+&(Q`bulBxFGtkYQ v,s2G(? LcD aaghVQtGte62b풿[끭U\DytY#ώET#z65Zka`L<%GM 6x~&ޡAR*ڏGÛ y Hoo5\-{3~ mYuoxl%t^5였yx.C.#B뤵3!7}\Ϭ 2(+}[`f.LNJ]Svu!vyo]crp_ΰuVSK2\FˈtWheuH" y=.fÿCPߒR7FBtFsrKܷPzİlk) n#}j:.HK_,M v+7C_΍b,G7(~{>iHUVQ0l:eX.!\RU =[tw$/L/?т2S5n$%:gIv"4r@W5sT zt+no Hy'&Xi_xGD=T֓.@?RǓ.|O,P̚*LaqT6j'vroY*ysd +fUL-?KO{.SFcuĵ0ɇz2mS0\_*r&-) #,{xur^~LRKc)n2YJQazI-ԿJN, TЯP 0U*% PE@b%ZH_. Z|sv0fXyύ* # hDZ\-HY8xN'3_W9Ll7ӡտc5PZJ=iG-Ӑ@\ʕlUƺZw B!|2MvoTeб}<S.`?Yo9'dS0ٗ|nj <* s/e T?K ?{H"MkY\p6׺k䌃Սz{ћ4_Ō]~(c4R"Q6=HFjX]-fkfZy9=FEJf×߸dK0:LhѯUgxqNc&mR7Xh5Vo5!K b^0G"뢉ҽg[X *mcFJX~0mӨ 7+ Q~7cr.X]؆z&X3>uRG}4`G8`Cn@w1o R;CZ@Lu{%ڑW}x9=a7߸|ݺYlFy?``20TG79TxE.qwSyв̩Gi߸7A&8#44a Pc~O} *D:WK:|) ̧N`1R ^0e'墻ؘأ/"w%D;ds:թg|5 ?c"!d KG?NhaRENC܊_J'1SJ6/!ӥP0%,}@t^4t ߷?A|d;E[3k`^Vq, (\_[JhJ' M^h .y)_#L_3X(V!I*TcO=କ1xj.S$@ j;Qĩi״-s:[F $~7\r$d2#D*; 'c"(HU:~ Fۤύ(XNZ-;0ӻFQCpoݳbKW },˸A"M{ ?XyъBV.i|㐁NKj"BЁƖ2 ;'rVM#&4ά. e;0ަy95P5!iP^U,xei(&ϙocZͭ`C+n|ɑ@e?aśS6㟺A4VjJT{aN9·76 @7Y%B8e[tcqyIЁYDU֝خCghy^[?Sy b2Z eӝz8ݗB\cˠW G|]I\]ކDf/3RGr&NxU+ Z‚ _f"GayXBr Bu1q东@85kR#b0qY,ߜr5M%@2>04 qlioι/D+uNN+K mR`hF@cƳ .()X\{~g!GKdytS2RJÌ! ]5x̯\KsVk벍@]op X߈ZQooAwY %*QBF6P\Fؠy=ܨNMPZnQ3n! ;&U8®ˠKz*^c|?/1^\ 2ymZ@Pe֓IF+ڷWWV@lf`4gt[Yh$6hE0&2o2U{.?jpls 3 5ܰ҇VWmqEpXNw(CFr5t{ a!,yw˓\ }Jv.ѥx0A^[?rVZ &Q:kT^(>܁ޤ2VJ>9n k 7CS7lNZ} jD|R`[ɪ86U[k"uh˔ Z@ڹfGm5ZJb.BUk~.M4,X`# D2v*MM!߼dĴP-X#Rpq:;mǤ\1d^Xl_ߘԼٵ{p).bW2\xav<e7gΥZ;Y$bLamT>%D^FM98[q{bZb'NNo8%l4TTÊ>܇aYX]CƵjqxZCbz7O|ϴ|/ k\hdVq9;_3:{I Wm!9G0'nx͙W'1 4sU{Wdp7>F*+P$rD]"P^#kR9r4y,7N_:ZZ x]JTisq:NrޓiI9h$)ZK0H `ss*&m!]&u}q }I@hw%e@ܫ"鈷|Og/ޥC< ! X=IQ3XuP 'eI%I)BvyW^]Zv/% ޼_޶J7z77n}2g=l)׶C#m:.qJmȴ./bǶKxLj~1)eaQm{LNk(Km’N'*ډI9q͆<,Ƃ.,+jCasOߖ_K$ m9UV,~>W$90gQLj)6*;9,}M{q{c;l?4}wv*X$ 2j{FX&c䓑*+/f<Ocqkq/kc %>7E'͕*a- 4dC2pC?O#Hsk;rY?>q +E?MPQeĴ" f _U%*O%,7Jgջ!myc(|kB/+( #Ӓx{i'9Lg\ ,XotD(Ʒs[4"D C<&r\uZy vUByKY pHl_1e@yNbQF'5WX7A.g\QX5L 4?k[ Xэz 7aHbL O7J`16THlb~,nӌڟ]dLcd*ɖ#[T9gfV'F{jA8NXm~5<\IƌQ|lX7Sv# P#D"=kΏIфH8k~) ]y88#da))_K1IJ FI,ylUelwc/A^psG%UY?+W< D `}FiрF/ ڒA״KXa>wDl^ rA2ik<{߭?S啃_0X=OlhsP p^+a A8i9 ?d@榡n-l16#v|,,+a5jQ~qNbRO:__|H-íwjo gA{{b^k_ׁtl㷎5cK*i%Tb܅Z-'|jWRs9rUٱyծ>vF@=~8|b%Ԝ6]Cdxy|MP|KjQ;枅=;`Cn 5rk:4HP y k6SmC/w[G2..XN!;c#v̕/wX!9JʿA|bb.R:'~~XpI0-#?I'QRsyiRsj'*3ՊƩl0~!3KBBOGXㅃdG ^ NoW|ec2MhZft8 /<-dJMIoQ=!0 XPXi%TWs GBEILhk6łU WzI3Pgk$mh/'ȈW EwZ4 =ˆrmÒ=$@PSwPR Gcr7OVlzj4kZ.wDS;-'Rp_!$J)yGZKUJ yJ kT?o"M&#|Ց+>boԋw#i*<yi`ˋ1,$١sZgAe}B.JKgzػ1kzCu5*:ݹp;X_)QCT+x6J#tZЎ$0NFkip8f#6=^Y>b`/fVkKLq_=_i'f`&Oqch?jq)+%߆+3zY0>TD_@tꙵ"7uc9wu9؀SurAp@ⷼuĸS _ޥ!$l)yᷛ)\n~2U5G]>˕ )?j˿H7; V|jzp#MrLb9,j0J5al^~K^4]No1˳huu2o8e3dy9~VZ'w8% l[n=bG ßn$hjTқ5/F,ļo4B88˲6ޕ, 6}yA0f҇wu#疹%Wv#fmB FR.L{$fFp|Gs0mM&JK tEC8'Ga8롥|5Df~mJ ScnAeҖe(BB9r/[V'ZZb.x>}:CE\Ǚ9q O;]vQ>V[ .oGNyi͸ wcI˛rG+Vĭ2WV:[8) %ӓXhE%-uRKO[!@ҡ&}#,II0HЇo>ݶ P7!]PzvQ0r>$1 aQNAt [aڨ =p6( E)*eʄBpBWQIUf Is*(~l-|unِAPiټW}7> k\ex)-tyh@-)ڝPjCdq8JlOل41p01 =?SA{U,[P&*y3B&]N/[g{ǯ^&Du:}Wr8j*dF[8`#etzѧ=_D,+OMrE=w,ϞR)G|f4(yG1q6%_Dht{ZڿDʭ뿚 ű8ޅLzJ&ZKČwɞw=tI:W Xx"d%yv;rsQ2 !7pPK#S(}ջ*ado(AFՔz5T2aN1CdՙU< CZC~Iy.0M&P"% >i-0M6,h#ߺі8:% ;`дfj.L5yyР/@'DM MR26{ITj!o3^O ̑KYGq%>G&ade; HG?^fXeV!u"`g?h &1r J9JkC&$%:Pl ٣) \$n7paA/ye"&SDV54ǺW5#l;CůcI][dqvGlp%RulBc5BlBtλֈw?3KBq9a>]j^,87X=+W–5 34cºj3/hۈ=fU\{V(<\j#7|DXIj—Pl|oDkCTtfـTJ8=Jwb䘽\0K&q$<\KO Jw()n^~/s.N,y Nsi Ah2:MS^ ob&Z~I/#Bf$PҾ?dCp݇&7a%GMo.pUItGCꌿffuBJ `/ po>%d pUW%I=޹8WLaBWkUcʋTvREFYC L+y;yM2؁}-BӉъ5Vax9h,%0;%7/{ڣ$)]_s u:7Y)c ZQ-=\y4W>.]"Ji/x ˕4#$yqGVq!e@ĉƴ(]/ ;蘥">S~N΁Y(F8[/|gxE$6A=<^,L+тtܢ C7!~A-& ԯ~X{IB3ӿN$AfnNd+€ӕ3E$ynxn@k\iSQȵ~t#NTLpMqQ  cOro^cFu Iװ9 ϊ # Tl>mB%턼Cj-#{Uew%9Nxe6S&&q1nחDnzF=&U.ݹn[{0Sp0K3IeJXv}k-}{]`wμ;wɚ bDk )t? G:uX3R(rYfgcGP_W(bV`ZP6t a$l/Qg,]0%n7tN|fDSH P86M5y7 /fAq Q2+F1[q^Yn=:2> gunӜZ_[u]fz(n"4)o-hKnL_mf˓_x >俆n~H"P|1Ū@7t犏jj4LlԤkCnXp% ߝ[Q,3^-و)MfəuZȺшRr^ Ptg9իJV~aw$|b:F&(R9RpV3܇4EDD9Ę9 wEQfQ\!ON`bi*! d C TJ;~.:C)M xOs,٩_v۸{S]?q8\P­<6Y[^iz;8 0}&sz#M?쀃bs $mVM$YRV}յ]t%7P`;evؼBTq *^Ư6I-W7Z M9=ThǩJL/[ZN`~2*AXyGX EC2Ҡ2:(xcP dm ]9ސ,@}C}o~C?ruy˯' b˛|&7Y.}L`C80܅tNJ:}|AM,71%'do]Hh&B )~, Qe)ҥf0I OiIJ zB\O^(< nk0N: [(b/@]LGxlB&o͐ AlB؍xnH5Q1fQ3l :,m%<,ms~M( JB (hCvn*ls;'~y6#?"~P͍._4Ґ;FZ۱L =f= ; *?>4>,)ɻF{<ȶh)Nr@N)vfx1Նhd ߭6 o~2le淚:Y.;(s")1/sL|rE*XnV ᳪJS=[,:X4y R&>UYyGA2R{2s!*7X@~DPܷ<[obs i|fiVT:$ɣ ɓ Oh*4ګԐHJP4ogOp팫 aIo'gR=$0G oOHL]w5o !)>[F|Ǣ8VBziΞa;nD$tN@ᚆ]yFQ|fl@wE!uܳψWOVdCde?Me_$@͸Rѵ N79Z#GJ$i5 lLN|o`x^i[nh~̹!"h/I/&<RڗC^һ|ԘfѱM2IGSeHBܙʷ\š,mw`./%^'" ̇xPlׇ.-Yv:AߘjDƳ%ǥPuYc9ov6q{ZMR?;S BX*Vm5Ku􁯉vjDOsO*Ѳ@.i.܏"an*E U* j?.-9R WFD9?SiН1僲#)ަNY"Zßy4*N;|CGѼgM!fn}t)7)0B!zքw5bqo/xV/]uճUX#ѾϣY𚣣MKOtI+WB7x>߹9d&O~Zgh/f7FMt#ZN#gd퓧9vC^B}K}YeXV1K­?& @lch!Pv51)O_ɡ}$+IMZw3.Ec~lbZTY5>u,hPa; ZNyaZD!l+[ Aj$b.ViejI_#PHj^J@c1!}J("*Kos77kZYֻIog;-ePM2i[E F&ߔ(<:;v/sb%jo]0x@ջ}cUR}RHI,G&xʣHLOo5& B>EI]FDq0U5̈Kː@xVc܃n¾eA)/f $T-v'4$9kU~zXc 1$n7bkq5U]!̰y%Jx(~[eVvsܠ* smyh+Yp,DmƨD DPꯡfa.+F0-C M9|.ژdP\ !u+WW Bzt||) O(v,\:wC)O0{_!QBv2`~1C,\z tJhA?o5i5[ł9NCw8L&5Rՠe.#:K*]-Am-8@("q~f3 kX\5jWݕ@\ ]XRg/-sWm)C+HpoiƒK<#4^򉉡KpZR8#HQo,eTp&Iool x+ϜVu3 )Z@]vn1ޡGPd2~OqM `Qe Ӑ6*NyK 28Wa@1  %-9,,@sn#/ .D#n-H3K0J ߁  \0k\952O Cysx?; U#p;~<U2isbUmLap7Yb6ezPSKTcq}ЊtA͠L<%|RSi˸54zHY0dYVqmy]n8kg=lK+dpvO ˋP1:u·Cq$Sҳg澂T!8u .&[Wu%kbuƧc@E'mGIMX}kx,+"{eAo+eH{8 3[.'˩×+_+,߈%"|4o8|Wc?@9,-V*UAճS#’ 0Ti~XЈ1qu  ֻd-EfU>[O㳙ݓE)ɔBumA_ \DN"%R/ADqSZe ރpK m ~8rvϬgyxzCa*xSp>QO3_a{}e|Q;y3"`r2wpE E|7[alb0`8Ɵd >*z-GhzNghY d +ZU792{Gǥ=Gɖg1[;pB(3ǻPBL)^ގM59'^;ryʍ/>цrCM4p4=F 2$@@74T+L0E"n5F}?L'A~=jfy񌹸a!#Bkhn 7mu!+YfΆC&_~k2"f'VoYVpG+Z"S5%Noc4\{|x1;t)&Ognķ0Y$rfXTE={94f7mIL^S ߆K=Ax}R%D:נk3|J}ۈ W*Vga!k_a^:&M4ą5p#hEF!5]Ԇf8_0fԓ6k WBz0]}_g/xD^ʪԂ.&=Y~8{2^S=D>ٱi/ =0-Q4'85E88SEuA@g`N\ ~EHs's?G*).C0e 1$FbޛkX\1OԊVYԋwƞ޻ft ^5i 3ꄁH!aܧ}U(WC#;< чT5×^<Ա vfxEΖ6;j?>YmBx&S ihОN#3%CpA=Zu!7 8lK(*9,k ՗s%})b|bf432\juz2 o~R ݮ[J`Osˤ?qݹ]zO(< *x=W5&\@G 2aU]B8Uw,*ybdMJufe#2gYa Ot/膺.xm*H97!qjC]WR\)hHN&TnIPt$̺׋]r{f"|Ⓦϕ؟1*B Ӄ( ٛfP5 s nR,*@i/4 r!~WOb#4%(X52g+|z޵k;Ӗ  `/n5I7(X@y!0^abf]U8ˈͫ8j\Tg͙ryX#URqyURmCIw:pW nחDEtU.G"_2Gغ-=Gc0P;Y=tl>JAX ĭ2$,h:tmh/\^qb *i2$$j8q3~@-;]֤VtBOXi!_>`.qW剼,žY@q8I:85d*#Tx4 ƛ;8uW'_׻,v7%AT֊PpE e7ouʞiC\R$qqcl6`#44"vi(벝q?n؁! UϩЎ.DR  1ifnZ)sj^ sp@cŐWh}P{k~$9@f^Wx]+]l$j?vA ,/PaMFU)%7vy?vMpPe[τ &us#$RpOSy(3 ~D O=ј;,e?ZVAں $^9@;a)vD܊'-t[6ƁA<fǾY"V0$;\+)I؝&ʶs\&n%Pp'k&"2'wJq!r]qGHE<0S,~ٻxi gPg9."s ICVC @N:!CXW\]8Y }%# TvnTOYܰLLWؚ,'- S5),%6{RCnQɮWBo 쿶+3C5XSnK2T@EWYmu#Q|}oIœϰٷ'xZns ts|=+Q`CMˡbx8թ}zzPw0i {NE bl͜)Pǚ)R{ŭf ]/(|8: OyP?&{s*ߔ7IJ.P UgSrx{2dNϱ́*%&u"X~Xѱt x $'4%{W1?U$sq|$Ϸ/n/iIM䴻zxye%;ಡWlR/MWsCNl_1K~R~8k\Xů+DX]COi, GT^T;$>KVŦ/e􌸼KÝyC$fRϥ-lҦnXT=LL K=ZKڪ]5X\҅QM[1Qa~FO0 L_9w­rtX"Vg(S|J탵,.}b zPv,; jH{bF@ ӑN::ژI B`SF_\܋Y_Ư v"J1Wo|.b_nAQ }H ~~婡nj2 '{ɣIUTu^Ғeq}QDYFqb[9-Ym1]GU|TnpK@gī"Swbe5tukC{Noro}EK`ըM܂bgԜ7I~8w3KHampS $>2\S2zB/jc饰a_GؓJE{N9w(7w_>%i{Aw#'IaP;3(Zv`rFI6,~(Y`5thpL8T4dƣhjptaNrEc^ܴ\JH{ϵtC @( L+Nz?)o|쯹QMJΙQ6ha~Lj#JYmG(BAGse o4gx$*w0xP'>::3Ǿ`'LVHW}6aV|$'Z5gKrS7^Ei9dYNuP1|Vڔ.Ǧ6"V)8o )x;T8uxgH 'MTԻޥgls5b>רnҹh{ǭAyG0\&χ()4ǍOD`ٙfO!.P;3y jܠ$cg\&s# ╕r&]YsE_-sz+_W B|cUT+e,XLx$([0Pc{.,Մ_kjal+]"7Ͱ BY^x%VqSX.<,qKK?S"VCd2W@o8@^Kۍ gK{3C+8 i2$i_p!6Yr>ml-Mh-vx MI3(8ЩmYJf~ ͇hOʉH`V#ÔR#.P9//? o_- /kC#mroXA#^Dw$nHxDH<K$ {,ʛ2X6","RMkA&qHk Viaf 3?IW#OqMnMY  Vϲn. }YnIM<$7 7 0<02UPiΦ#I Ob=),T3m֣ﵶGOvkSblj`UOdZYFGeRĆnx>ݖt1&]Ew}=(j8F)1O.-^\C=a0W3%4,l ɴ+Hh2j&ϳJS ݚ ?`(C;m:%|w*ήwFYZ-J"KEjX@?+3#]n0RJ0W\%h?*-v} |pȫmߗzXZV@e8<>QQ)mn7 u4.i^oV3)?'>kgEILWڰ@ Gl]Dޟe& L_$z0m kgܹ׃}R*s&L3jx'IZ`K^}1v͐PU> |hN'nE@yMpN5\SU3)+fÉf%G'O#C,;wzRp͏4"K%(n׶Qi;sʋu@Cؘ8b^gW5܅#:fN9{F^a'Vv82|M,EgҬhW [O" w#]~$@PrC>+3M+axh,;h`h|׮mi\ro je1u9s6 ]9>׷tO|!r30?Ef|InW4/JrM(RX‚nΔGcf|"r˕-E$vfGg\L i_?ܯ*޸0XA%ml2*-;e;ƝoD4Ҷ'0kkJxF {X6IޣB>ܪ&6[,ݵ^BO='kz~ r5UB}onYuwKZN6:] -$W-I paLvOmDך"qRjEbZV:x;P{nϣ9qվ{dLS~3#tihTu;u%7>gU t@ye8ܓMI\CPN ȥK^޷|1eF+4G1(.4Eq% 3U>xT*oaJx5`*e`Ż $v[-ƪ@rSo q&tggi1C}O?$PgEZ>%Ț$K#~c+F05˩gO<ِz, U Nϴx@I{HMT-Ж 6 &US' nunB8_וE=Ї 1j)JE9 C!Iv`-PaG=*,OHmu&!SDm5W~[^̗z*)8́Lhkd0<( `6V3EdJ×^VnmO9<),=%ڐ I|jbw|bpapsϴgљ-Lk2}/O3^giD`vT!/~ZסrlDL1zӱ{Ydt..̨Ii#X c3C@v6#aPcc~n5 @D貒LJž1:a#y6Ni{7Xܲl~ ^+ی}JCw*rYI$Z^-Aw:.Dvk(x/N_dž{2 a٤.vNؕ+ڇ.1I7 (U{MA'yYP0=扮B_ap(Lvy Ta|amS՝`.YvOB`[P@MJC!2#-KVڭ њq.02m9T, AƛC$ׅw+)Ͼ mA'Yn<#w.-ybƒTU}!BQ5 ӈ ̲,|Q="zFQ5*ƙq_JGdkN(-|퍸۝? &e=3 WD㊕vgm0b~Ga)\zbUI0{f;e랋,e^R4q<wױ ;ȶ0C4L# LSY2}9i?J;,.5X{$ G:#xi)ov#6||Ŀgj*jDCQ{، LƒX SGÌZ睴y$@cM`*pIJPk Ha}?)]D>9q-CSbDU^k D ; S=C1 ď}b(lvG n@שwYMAcRl PU'o剦iUA9ӵ&UMg|L+2(M d9sۡ4xȓ) TpaEޢ_Mm07:L tWilxʫ(' No"Vvט ?>E hgLH:DXycnaK,Hvm #{%@4#jl<o~ry^=}BGqs u$߲LV'0<64#$K|X2o =H ?񲬞h GQ*S]{1a-7Xg044#LR+)olWQ_|{~w˯[ɍ3WXF;{5$e73gF!m*@Vq}vOϺQ2"U*S5INF]F.M$!}c$p?V{!4,?l %zJvc<:Bge! ;-{$]Jʞ| (37\`ϷT!lYFW&_b\nIFYD{P2 2勴6#4vJ@ꬬ1m5Ц1uzfv.R}A?ʧ=d_Yck j`dZ8 ҨiJK㟇Xӱ28& h"LK/$X1k=+Dԁ,]d97>E0+]ƍ8)td23 ы`5泄jI}V2p(w4-ړ)-+ w>&PFcf>/ˌg99jӫ4q&+Ws^7쵺,Ʌ>quJPfm7ثM+<B& ]l!t'XM"|ߔ~(n9 }sf-Y//fP / R,NkH0}s>-o_KmYDG(t?p^Tٵ-PAo7,_폢^p+N8!hAqGKi̇>+Ֆjo׵%8%m*>>ЀF 0.rao!,=S5 #H_~?J,r|,'ޡ3NB!0ae'cHoUQĨ-5oDMC(MA#N%]tљ{}.x<_5[hޮd@29J8`uJ . &Z %uDׅxRiX52*+H=/39[@#,hZ!*7jG&24[ r0bD /'hz]6>n+b_8?n9.BxQl9/?RvH L~^WʑB J{(S6+#fy7'^RzQHL*v?7(@(>݂h3L8 ׃Q٫a$y,=+^4s*:3tP:t7 7VJo2g{=;ɬ#v$2*h"-"X O p?И[$ ѵ:vIlz% AL㠌ةcylF=iuor:%b-u43n#f$xr٧[m1ơu=12¡So jvNzFwXFM̸:BRG_=x x7Mp'cMyUB5J.jw[uV %B]p"f"H1g$ց؆"`އ@!1LЂ&x"=>ep&eA0$K3|xj5?G8Y%;**Q;o.]єXw-Nxdgay.u]Wl,@ \tTI2C::_E Vibr k `9t'GYaW7@Z(]ٵ YU~xW@Z{y> L ց Evl)@э7Na."$IYpymb<+. ujrD  pSaJB#}ܛx#r|a@R)@mNW ˁJ^zx4C 5i}T~@!%xy^O\giÕ˰2nps깇om OkϺACt9rHH- EQ7(kk &^;zK|mOA~ wt6tNI;(KWSmI۩ hv6xL۪ܹ%p [#c?#f qZ#l%E%<-"rL T5@+1~w8l:.g>ɓqyX/m, bU t?6sg6-rĨZ燲-QVȂRuq;R~@R;^ӌ [e.&\00yDh%]nɄ z1}p"?8 ]a-b?cm0]FdOEZgRhPŎ1P//E S!{`!NP0 ^ߙu|"CpGz'7DEbY[jZ܌H80% =v'aPtu]mrJ"2o'V;w0_Ez KzK&G;r9| @((Mu}ۉtu]p }JBJ>R3c)\6\6IՕA7ԝ${H!o5 D8Ckr)uX" o`)bNa n?%mr/DFgngX<;;t6PYOtM7o@y]\a5 H(B.:2̉zZiVrO4NCWm{WsN}'p9$-%P_2;ŗND[ϜblQMn8Hj:0}2vF(RO .OWrB2@ON7+\BvnzɃ%M{t˙w k Yɖkc4S3?&W٬q.KpF7:M~"b I3b# R~I<8hQ#e&+u@4꧸)G$ciT=< LSm 5$bs<$hnEۊo1UKk-Do7P}Q:pJW &ER6ga$z.) 5~'_RRkԝNtkmar j$O\ }NDF{I*rjz°|PL CCAOr0$^Ec}}0Wg즲2|ږ _7!)Gil݋6uTqOh[6ؤ.3X!9|*7V*J#cky,-3{ SO G\ %|/X`^[яMk/!~@/>UY+[*_֛ ف{S~:= ȳ(ްBk$')Ƚ5kLHwGZj(wJզ+i~މ'zAІ@ՓC`xbZ=ޡu*bӊPL{OMi@9W9> A8llBW'ԳC0v33&`b3BYݪ"vwEJkJlѴHLk<"Pkf;qWci %nzYAH޴?Cok `œ`t2n_\` b 2,>VV<<]e|ljX^GԞ#sS2hyR@B  ߖ9M @3u#o_Vs+y`r<$ 4mQ gNY]X+-^n#k dU 3%)΄͈[&S+]2"J qoP.#%%RESA^,{w4bJp_|u ڥZz\QQue"emFJ{3<֕ ,J-FC+{v<_p\/`K[B[ ј6w@07jE/NJ$1S(!AcS"Zd=wͥypw=@. \uYg׼-9NK:-S6^]!ޯƈ B2sA U}AHmRĻ TN߁\  posc:mGݵdE&C6eSpO2B`i?2VH0fyߢw5N.0Qg-Z4= ZFG[ 4!U}Z1xJa'a?&#Vf&ߠye6MJ,sπgU/AېO +(\,~Hϐ{}&=Z? IZLhǼ߆C7%ҧ3QC%ξRQ:sPџyt>6;)@E`̹ HB  p0DAj7Hc9 l23^W_p%5!O8+Eŝ+v0;PwPҨzܦP&B]Z cx 꺨b4M.ǏRxdcPq]QuΨu(C\34 Ti$^w]QŎ#a%zedd[Nhxdu;4?; 'dGI-fĻ1|pgJtU0nʋ