#include <bits/stdc++.h>
template <typename T1, typename T2>
std::ostream& operator<<(std::ostream& os, const std::pair<T1, T2>& p) {
  return os << "(" << p.first << ", " << p.second << ")";
}
template <typename C>
std::ostream& _debug_go(std::ostream& os, const C& v) {
  bool first = true;
  os << "[";
  for (auto i : v) {
    if (!first) os << ", ";
    os << i;
    first = false;
  }
  return os << "]";
}
template <typename T>
std::ostream& operator<<(std::ostream& os, const std::vector<T>& v) {
  return _debug_go(os, v);
}
template <typename T>
std::ostream& operator<<(std::ostream& os, const std::set<T>& v) {
  return _debug_go(os, v);
}
template <typename T1, typename T2>
std::ostream& operator<<(std::ostream& os, const std::map<T1, T2>& v) {
  return _debug_go(os, v);
}
struct Debugger {
  template <typename T>
  Debugger& operator,(const T& v) {
    std::cerr << v << " ";
    return *this;
  }
} dbg;
using namespace std;
using uint = uint32_t;
using llint = int64_t;
using ullint = uint64_t;
template <typename T = int>
using Pt = complex<T>;
template <typename T>
T cross(const Pt<T>& a, const Pt<T>& b) {
  return a.real() * b.imag() - a.imag() * b.real();
}
struct UnionFind {
  const int N;
  vector<int> p, size;
  vector<vector<int>> comps;
  UnionFind(int N) : N(N), p(N), size(N, 1), comps(N) {
    iota(p.begin(), p.end(), 0);
    for (int u = 0; u < N; ++u) {
      comps[u] = {u};
    }
  }
  int find(int u) {
    if (p[u] == u) return u;
    return p[u] = find(p[u]);
  }
  int unite(int u, int v) {
    u = find(u), v = find(v);
    if (u == v) return u;
    if (size[u] < size[v]) swap(u, v);
    p[v] = u;
    size[u] += size[v];
    for (int vv : comps[v]) {
      comps[u].push_back(vv);
    }
    comps[v].clear();
    assert(size[u] == comps[u].size());
    return u;
  }
};
struct Graph {
  const int N, C;
  vector<vector<pair<int, int>>> E;
  UnionFind strictDoubleRainbow;
  vector<map<int, int>> nbr;
  vector<set<int>> nbrComps;
  Graph(int N, int C)
      : N(N), C(C), E(N), strictDoubleRainbow(N), nbr(N), nbrComps(N) {}
  void addEdge(int x, int y, int z) {
    set<tuple<int, int, int>> done;
    for (const auto [x, y] : vector<pair<int, int>>{{x, y}, {y, x}}) {
      auto [it, inserted] = nbr[x].insert({z, y});
      if (!inserted) {
        int a = strictDoubleRainbow.find(it->second);
        int b = strictDoubleRainbow.find(y);
        if (a == b) continue;
        if (strictDoubleRainbow.size[a] < strictDoubleRainbow.size[b]) {
          swap(a, b);
        }
        for (int v : strictDoubleRainbow.comps[b]) {
          for (auto [u, _] : E[v]) {
            bool ok =
                nbrComps[u].erase(b) || done.find({u, a, b}) != done.end();
            assert(ok);
            nbrComps[u].insert(a);
            done.insert({u, a, b});
          }
        }
        strictDoubleRainbow.unite(a, b);
      }
    }
    E[x].push_back({y, z});
    nbrComps[x].insert(strictDoubleRainbow.find(y));
    E[y].push_back({x, z});
    nbrComps[y].insert(strictDoubleRainbow.find(x));
  }
  bool doubleRainbowConnected(int x, int y) {
    return strictDoubleRainbow.find(x) == strictDoubleRainbow.find(y) ||
           nbrComps[y].find(strictDoubleRainbow.find(x)) != nbrComps[y].end();
  }
};
struct GraphSlow {
  const int N, C;
  vector<vector<pair<int, int>>> E;
  GraphSlow(int N, int C) : N(N), C(C), E(N) {}
  void addEdge(int x, int y, int z) {
    E[x].push_back({y, z});
    E[y].push_back({x, z});
  }
  bool doubleRainbowConnected(int x, int y) {
    vector<int> vis(N);
    queue<int> q;
    q.push(x);
    vis[x] = true;
    while (!q.empty()) {
      int u = q.front();
      q.pop();
      if (u == y) return true;
      for (auto [u0, z0] : E[u]) {
        if (u0 == y) return true;
        for (auto [u1, z1] : E[u0])
          if (z0 == z1) {
            if (!vis[u1]) {
              q.push(u1);
              vis[u1] = true;
            }
          }
      }
    }
    return false;
  }
};
const int MAX_N = 1e5, MAX_Q = 2e5, MAX_C = 1e5;
struct GraphOp {
  bool addEdgeOrQuery;
  int x, y, z;
  friend ostream& operator<<(ostream& out, const GraphOp& op) {
    if (op.addEdgeOrQuery) {
      return out << "(+ " << op.x << " " << op.y << " " << op.z << ")";
    } else {
      return out << "(? " << op.x << " " << op.y << ")";
    }
  }
};
struct GraphOps {
  int N, C;
  vector<GraphOp> ops;
  friend ostream& operator<<(ostream& out, const GraphOps& ops) {
    out << "(" << ops.N << ", " << ops.C << ", \n";
    for (const auto& op : ops.ops) {
      out << op << ",\n";
    }
    return out << ")";
  }
};
int main() {
  ios_base::sync_with_stdio(false);
  cin.tie(nullptr);
  int N, M, C, Q;
  cin >> N >> M >> C >> Q;
  Graph g(N, C);
  for (int i = 0; i < M; ++i) {
    int x, y, z;
    cin >> x >> y >> z;
    --x, --y, --z;
    g.addEdge(x, y, z);
  }
  for (int q = 0; q < Q; ++q) {
    char tp;
    cin >> tp;
    int x, y;
    cin >> x >> y;
    --x, --y;
    if (tp == '+') {
      int z;
      cin >> z;
      --z;
      g.addEdge(x, y, z);
    } else {
      cout << (g.doubleRainbowConnected(x, y) ? "Yes" : "No") << '\n';
    }
  }
  return 0;
}
