diff --git a/filecache.inc b/filecache.inc
index ce6ec6f..acceeb5 100644
--- a/filecache.inc
+++ b/filecache.inc
@@ -15,9 +15,38 @@ function filecache_directory() {
   return $filecache_directory;
 }
 
+/**
+ * Sets encoding method for a cache bin (if the bin indeed uses a File Cache).
+ *
+ * This is a convenience function, which can be called from hook_init()
+ * for most bins, in case the default encoding for the bin isn't desired.
+ *
+ * If this .inc file is added to $conf['cache_backends'] in settings.php,
+ * this function should be called like
+ *   if (function_exists('filecache_set_encoding')) {
+ *     filecache_set_encoding($bin, $encoding);
+ *   }
+ * i.e. not using module_exists(), because file caching will still be active
+ * even when the module is disabled.
+ *
+ * @see DrupalFileCache::set_encoding()
+ *
+ * @param $bin
+ *   The cache bin for which the filename encoding should be altered.
+ * @param $method
+ *   A specifier for the encoding method.
+ */
+function filecache_set_encoding($bin, $method) {
+  $obj = _cache_get_object($bin);
+  if ($obj instanceof DrupalFileCache) {
+    $obj->set_encoding($method);
+  }
+}
+
 class DrupalFileCache implements DrupalCacheInterface {
   protected $bin;
   protected $directory;
+  protected $encoding_method;
 
   function __construct($bin) {
     if (empty($bin)) {
@@ -29,6 +58,7 @@ class DrupalFileCache implements DrupalCacheInterface {
 
     $this->bin = $bin;
     $this->directory = filecache_directory();
+    $this->set_encoding();
     $t = get_t();
 
     // Check for problems with filecache_directory
@@ -94,9 +124,135 @@ class DrupalFileCache implements DrupalCacheInterface {
   }
 
   /**
+   * Set encoding method for Cache IDs (to filenames).
+   * If no method identifier is given, a sane default will be chosen.
+   *
+   * If you want to influence a cache bin's encoding from your module, call
+   * filecache_set_encoding() BEFORE any cache_get() calls are made for that
+   * bin. hook_init() is a good place, for most bins.
+   *
+   * Methods are:
+   * - 'flat' / any unrecognized method (default):
+   *   All Cache IDs end up in the same directory. The filename will be the
+   *   ID which is urlencode()'d, except the oft-used separators ':' and "/'
+   *   are represented by ' ' and '^' characters (for readability and filesystem
+   *   compliance).
+   *
+   * - any numeric argument (N):
+   *   The cache bin directory will have one level of subdirectories;
+   *   The first N characters are converted to the subdirectory name.
+   *   (This works well for e.g. a large or unknown amount of numeric Cache IDs;
+   *   the ideal value for N will depend on the number of cached items. It should
+   *   be increased if the number of cached items grows exponentially, to keep
+   *   the number of files per directory in check.)
+   *
+   * - '/' or ':':
+   *   The '/' or ':' character in a Cache ID is used as a directory separator.
+   *   The number of nested directories therefore depends on the Cache IDs.
+   *   (This works well for cache bins with a large number of hierarchically
+   *   structured IDs)
+   *
+   * - the previous, followed by a number (e.g. ':1' or '/2'):
+   *   Same, but the directory tree is flattened to a maximum of N nested
+   *   subdirectories.
+   *   (This works well for cache bins with a smaller number of hierarchically
+   *   structured IDs; the flattening makes for better file system overview.)
+   *
+   * - '::'
+   *   Like ':', but do not turn occurences of '::' into 2 directories deep;
+   *   Cache IDs like 'root:id' and 'root::id' will be in distinct directories
+   *   at the root level.
+   *   (This gives somewhat better file system overview for bins with IDs that
+   *   have very many '::' occurences, which would yield in nested directories
+   *   where the first level directory only contains one subdirectory.)
+   *
+   * @see encode_cid()
+   */
+  function set_encoding($method = '') {
+
+    // A whole lot of code just to set defaults, and comments on why.
+    if (empty($method)) {
+      switch ($this->bin) {
+        case 'cache_path':
+          // Directory structure will match website paths
+          $method = '/';
+          break;
+
+        case 'cache_field':
+          // Directory structure will match field types, names, etc
+          $method = ':';
+          break;
+
+        case 'cache_menu':
+          // Number of items in this cache is not so high, so
+          // flatten the hierarchy.
+          $method = ':1';
+          break;
+
+        case 'cache_filter':
+        case 'cache_update':
+          // Certainly cache_filter has lots of '::'s and for many sites
+          // there is never a (language) ID in between the 2 colons. So flatten.
+          // cache_update has just been put here after a first glance.
+          $method = '::';
+          break;
+
+        // For entity caches we'll make the first N characters into separate
+        // directories. The ideal value of N really depends on how many entities
+        // are on the site. A value that is too high will look silly and is not
+        // ideal for performance; a value that is too low can really have a
+        // negative impact on performance.
+        // So the below values are guesses at best default values.
+        // Uninformed guesses: Where E is number of entities:
+        // 'flat' is best until about 100 directory entries
+        // '1' will contain 21- entries at top level (10 files and 11- dirs);
+        //     ~ E/10 entries per subdir => is best until E ~ several thousand
+        // '2' will contain 201- entries at top level; ~ E/100 entries
+        //     per subdir => is best until E ~ 100.000
+        // '3' will contain 2001- entries at top level; ~ E/1000 entries
+        //     per subdir => after you reach ~ 5.000.000 you should really
+        //     start thinking about adding another level of dirs.
+        // Obviously the number of subdirs created depends on cache lifetimes
+        // and on which 'range' of entities is being cached, so it's all a bit
+        // unpredictable.
+        case 'cache_entity_taxonomy_vocabulary':
+          $method = 'flat';
+          break;
+
+        case 'cache_entity_taxonomy_term':
+          $method = '1';
+          break;
+
+        default:
+          if (strpos($this->bin, 'cache_entity_') === 0) {
+            $method = '2';
+          }
+          else {
+            // Default method: the 'flat' one.
+            // (This method name is not referenced anywhere but it seems
+            // better to have an explicity defined one, to reference in
+            // documentation.)
+            $method = 'flat';
+          }
+      }
+    }
+
+    // Actually set the encoding.
+    $this->encoding_method = $method;
+  }
+
+  /**
    * Returns string, suitable for using as filename for caching data.
    * Can explicitly also be used for encoding the first part of a Cache ID.
    *
+   * Note: this class has tried to implement several sane defaults with
+   * set_encoding(), but if you have a better idea that suits your purpose
+   * which isn't covered by the current encodings, you can
+   * - implement your own caching .inc file which you refer to from settings.php
+   * - ...with a subclass of DrupalFileCache
+   * - ...which implements your own version of encode_cid() (and possibly
+   *   ignores set_encoding())
+   *
    * @param $cid string
    *   Cache ID (or first part)
    * @return string
@@ -104,16 +260,85 @@ class DrupalFileCache implements DrupalCacheInterface {
    *   corresponding to the first part of a cache ID.
    */
   protected function encode_cid($cid) {
-    // Use urlencode(), but turn the
-    // encoded ':' and '/' back into ordinary characters since they're used so
-    // often. (Especially ':', but '/' is used in cache_menu.)
-    // We can't turn them back into their own characters though; both are
-    // considered unsafe in filenames. So turn : -> <space> and / -> ^
-    $id = str_replace('%2F', '^', str_replace('%3A', ' ', rawurlencode($cid)));
+    // If we create trees from the $cid, there are some challenges, resulting
+    // from the fact that there are no restrictions on $cid:
+    // * We should be able to store (separate) values for
+    //   - foo
+    //   - foo:
+    //   - foo:1 (meaning 'foo:' should be both a directory and a file)
+    //   - foo::1
+    // * And we should still take care that e.g. the encoded 'foo:' is a
+    //   substring of the encoded 'foo:1' (see comment at function definition),
+    //   so don't do string replacements which negate that.
+    //
+    // This basically amounts to: you always need to suffix a directory name
+    // with some identifier (that does not clash with another file), so that
+    // - 'equal' file and directory can coexist
+    // - the 'shorter' of the two is always the filename.
+
+    if (is_numeric($this->encoding_method)) {
+      if (strlen($cid) <= $this->encoding_method) {
+        // Make a flat filename (no subdirectories).
+        // We can't turn them back into their own characters though; both are
+        // considered unsafe in filenames. So turn : -> <space> and / -> ^§
+        $id = $this->encode_part($cid);
+      }
+      else {
+        // Turn the first N characters into a directory.
+        // Suffix directory names with a single '%' to overcome challenges.
+        // (This is unambiguous for urlencoded strings.)
+        $id = $this->encode_part(substr($cid, 0, $this->encoding_method)) . '%/' .
+          $this->encode_part(substr($cid, $this->encoding_method));
+      }
+    }
+    else {
+      // Default: all flat filenames.
+      $id = $this->encode_part($cid);
+
+      // Now take care of special selectors, which make $fileid into a directory tree.
+      switch (substr($this->encoding_method, 0, 1)) {
+        case '/':
+          // Turn specified characters into directory separators, except the
+          // character at end-of-string. encoding_method can specify a numeric
+          // modifier for maximum tree depth (see comment at set_encoding()).
+          $max = intval(substr($this->encoding_method, 1));
+          if ($max) {
+            $id = implode('^%/', explode('^', substr($id, 0, strlen($id) - 1), $max + 1))
+              . substr($id, -1);
+          }
+          else {
+            $id = str_replace('^', '^%/', substr($id, 0, strlen($id) - 1)) . substr($id, -1);
+          }
+          break;
+
+        case ':':
+          // Same thing for ':'....
+          $max = intval(substr($this->encoding_method, 1));
+          if ($max) {
+            $id = implode(' %/', explode(' ', substr($id, 0, strlen($id) - 1), $max + 1))
+              . substr($id, -1);
+          }
+          else {
+            $id = str_replace(' ', ' %/', substr($id, 0, strlen($id) - 1)) . substr($id, -1);
+          }
+          // ...but with one extra modifier: if encoding method is '::', do not
+          // turn occurences of '::' into 2 directories deep (see set_encoding())
+          if ($this->encoding_method == '::') {
+            $id = str_replace(' %/ %/', '  %/', $id);
+          }
+      }
+    }
 
     return $id;
   }
 
+  private function encode_part($cid) {
+    // Use urlencode(), but turn the encoded ':' and '/' back into ordinary
+    // characters since they're used so often. (Especially ':', but '/' is used
+    // in cache_menu.) See 'flat' comment at set_encoding().
+    return str_replace('%2F', '^', str_replace('%3A', ' ', rawurlencode($cid)));
+  }
+
   /**
    * Returns (base) directory for this cache bin,
    * as absolute directory with trailing slash
@@ -259,6 +484,12 @@ class DrupalFileCache implements DrupalCacheInterface {
     if ($fh === FALSE) {
       // If file doesn't exist, create it with a+w permissions
       $fh = fopen($filename, 'c+b');
+      if ($fh === FALSE) {
+        // The prefix' exact directory may not exist. Retry.
+        if (@mkdir(dirname($filename), 0777, TRUE)) {
+          $fh = fopen($filename, 'c+b');
+        }
+      }
       if ($fh !== FALSE) {
         if (!chmod($filename, 0777)) {
           watchdog('filecache', 'Cannot chmod %filename',
@@ -350,26 +581,59 @@ class DrupalFileCache implements DrupalCacheInterface {
    * @param string $cid_prefix
    *   The prefix/wildcards
    * @return array
-   *   A list of existing filenames (which could contain directories),
-   *  with basedir included
+   *   A list of existing filenames / subdirectories, prefixed with basedir.
    */
   protected function all($cid_prefix = '') {
-    $exclude = array('.', '..');
-    $list = array();
+    $prefix_dir = $this->basedir();
     $filename_prefix = '';
     if ($cid_prefix !== '') {
       $filename_prefix = $this->encode_cid($cid_prefix);
+      // Resolve $filename_prefix to the deepest directory
+      $dir = dirname($filename_prefix);
+      if ($dir !== '.') {
+        $prefix_dir = $this->basedir() . $dir . '/';
+        $filename_prefix = substr($filename_prefix, strlen($prefix_dir) + 1);
+      }
     }
+
+    return $this->dir_recursive($prefix_dir, $filename_prefix);
+  }
+
+  /**
+   * Return all filenames in (sub)directory which match a certain prefix
+   * (Note: could be a static function.)
+   *
+   * @param $dir string
+   *   Directory to search, must end with slash
+   * @param $filename_prefix string
+   *   Prefix to match, or '' for all files
+   * @return array
+   *   Filenames, including directory name at the start. If $filename_prefix is
+   *   '', directory names will also be included, following contained filenames.
+   */
+  protected function dir_recursive($dir, $filename_prefix) {
     $filename_prefix_len = strlen($filename_prefix);
-    $dh = opendir($this->basedir());
+    $exclude = array('.', '..');
+    $files = array();
+    $dh = opendir($dir);
     while (($filename = readdir($dh)) !== FALSE) {
       if (($filename_prefix === '') ? !in_array($filename, $exclude) :
           strncmp($filename, $filename_prefix, $filename_prefix_len) === 0) {
-        $list[] = $this->basedir() . $filename;
+        // File or dir matches the prefix
+        if (is_dir($dir . $filename)) {
+          // Match all files in this subdirectory
+          $files = array_merge($files, $this->dir_recursive($dir . $filename . '/', ''));
+        }
+        else {
+          $files[] = $dir . $filename;
+        }
       }
     }
     closedir($dh);
-    return $list;
+    if ($filename_prefix === '' && !empty($files)) {
+      $files[] = substr($dir, strlen($dir) - 1);
+    }
+    return $files;
   }
 
   protected function delete_wildcard($cid_prefix) {
@@ -377,7 +641,13 @@ class DrupalFileCache implements DrupalCacheInterface {
     timer_start('filecache_delete_wildcard');
     foreach ($this->all($cid_prefix) as $filename) {
       @unlink ($this->directory . '/' . $filename);
+      if (is_dir($filename)) {
+        @rmdir($filename);
+      }
+      else {
+        @unlink($filename);
       ++$nr_deleted;
+      }
     }
     watchdog('filecache', "delete_wildcard('%w') for bin '%b' finished in !t ms, deleting !nr files",
       array('%w' => $cid_prefix, '%b' => $this->bin, '!t' => timer_read('filecache_delete_wildcard'), '!nr' => $nr_deleted));
@@ -406,11 +676,24 @@ class DrupalFileCache implements DrupalCacheInterface {
     $nr_deleted = 0;
     timer_start('filecache_delete_expired');
 
+    $dir_nonempty = FALSE;
+
     foreach ($this->all() as $filename) {
+
+      if (is_dir($filename)) {
+        // All files in this dir have been processed.
+        if (!$dir_nonempty) {
+          @rmdir($filename);
+        }
+        $dir_nonempty = FALSE;
+        continue;
+      }
+
       // XXX reads all entries XXX
       $content = @file_get_contents($filename);
       if ($content === FALSE) {
         // Some problem in reading, but we don't care.
+        $dir_nonempty = TRUE;
         continue;
       }
 
@@ -423,8 +706,11 @@ class DrupalFileCache implements DrupalCacheInterface {
            && ($creation === 0 || $cache->created < $creation))) {
         @unlink($filename);
         ++$nr_deleted;
+        continue;
       }
-      elseif ($calc_size) {
+
+      $dir_nonempty = TRUE;
+      if ($calc_size) {
         // gather statistics
         $stat = @stat($filename);
         if ($stat === FALSE) {
